mirror of https://github.com/Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
556 lines
15 KiB
556 lines
15 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
// |
|
// mcp251xfd - Microchip MCP251xFD Family CAN controller driver |
|
// |
|
// Copyright (c) 2019, 2020 Pengutronix, |
|
// Marc Kleine-Budde <[email protected]> |
|
// |
|
|
|
#include "mcp251xfd.h" |
|
|
|
#include <asm/unaligned.h> |
|
|
|
static const struct regmap_config mcp251xfd_regmap_crc; |
|
|
|
static int |
|
mcp251xfd_regmap_nocrc_write(void *context, const void *data, size_t count) |
|
{ |
|
struct spi_device *spi = context; |
|
|
|
return spi_write(spi, data, count); |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_nocrc_gather_write(void *context, |
|
const void *reg, size_t reg_len, |
|
const void *val, size_t val_len) |
|
{ |
|
struct spi_device *spi = context; |
|
struct mcp251xfd_priv *priv = spi_get_drvdata(spi); |
|
struct mcp251xfd_map_buf_nocrc *buf_tx = priv->map_buf_nocrc_tx; |
|
struct spi_transfer xfer[] = { |
|
{ |
|
.tx_buf = buf_tx, |
|
.len = sizeof(buf_tx->cmd) + val_len, |
|
}, |
|
}; |
|
|
|
BUILD_BUG_ON(sizeof(buf_tx->cmd) != sizeof(__be16)); |
|
|
|
if (IS_ENABLED(CONFIG_CAN_MCP251XFD_SANITY) && |
|
reg_len != sizeof(buf_tx->cmd.cmd)) |
|
return -EINVAL; |
|
|
|
memcpy(&buf_tx->cmd, reg, sizeof(buf_tx->cmd)); |
|
memcpy(buf_tx->data, val, val_len); |
|
|
|
return spi_sync_transfer(spi, xfer, ARRAY_SIZE(xfer)); |
|
} |
|
|
|
static inline bool mcp251xfd_update_bits_read_reg(unsigned int reg) |
|
{ |
|
switch (reg) { |
|
case MCP251XFD_REG_INT: |
|
case MCP251XFD_REG_TEFCON: |
|
case MCP251XFD_REG_FIFOCON(MCP251XFD_RX_FIFO(0)): |
|
case MCP251XFD_REG_FLTCON(0): |
|
case MCP251XFD_REG_ECCSTAT: |
|
case MCP251XFD_REG_CRC: |
|
return false; |
|
case MCP251XFD_REG_CON: |
|
case MCP251XFD_REG_FIFOSTA(MCP251XFD_RX_FIFO(0)): |
|
case MCP251XFD_REG_OSC: |
|
case MCP251XFD_REG_ECCCON: |
|
return true; |
|
default: |
|
WARN(1, "Status of reg 0x%04x unknown.\n", reg); |
|
} |
|
|
|
return true; |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_nocrc_update_bits(void *context, unsigned int reg, |
|
unsigned int mask, unsigned int val) |
|
{ |
|
struct spi_device *spi = context; |
|
struct mcp251xfd_priv *priv = spi_get_drvdata(spi); |
|
struct mcp251xfd_map_buf_nocrc *buf_rx = priv->map_buf_nocrc_rx; |
|
struct mcp251xfd_map_buf_nocrc *buf_tx = priv->map_buf_nocrc_tx; |
|
__le32 orig_le32 = 0, mask_le32, val_le32, tmp_le32; |
|
u8 first_byte, last_byte, len; |
|
int err; |
|
|
|
BUILD_BUG_ON(sizeof(buf_rx->cmd) != sizeof(__be16)); |
|
BUILD_BUG_ON(sizeof(buf_tx->cmd) != sizeof(__be16)); |
|
|
|
if (IS_ENABLED(CONFIG_CAN_MCP251XFD_SANITY) && |
|
mask == 0) |
|
return -EINVAL; |
|
|
|
first_byte = mcp251xfd_first_byte_set(mask); |
|
last_byte = mcp251xfd_last_byte_set(mask); |
|
len = last_byte - first_byte + 1; |
|
|
|
if (mcp251xfd_update_bits_read_reg(reg)) { |
|
struct spi_transfer xfer[2] = { }; |
|
struct spi_message msg; |
|
|
|
spi_message_init(&msg); |
|
spi_message_add_tail(&xfer[0], &msg); |
|
|
|
if (priv->devtype_data.quirks & MCP251XFD_QUIRK_HALF_DUPLEX) { |
|
xfer[0].tx_buf = buf_tx; |
|
xfer[0].len = sizeof(buf_tx->cmd); |
|
|
|
xfer[1].rx_buf = buf_rx->data; |
|
xfer[1].len = len; |
|
spi_message_add_tail(&xfer[1], &msg); |
|
} else { |
|
xfer[0].tx_buf = buf_tx; |
|
xfer[0].rx_buf = buf_rx; |
|
xfer[0].len = sizeof(buf_tx->cmd) + len; |
|
|
|
if (MCP251XFD_SANITIZE_SPI) |
|
memset(buf_tx->data, 0x0, len); |
|
} |
|
|
|
mcp251xfd_spi_cmd_read_nocrc(&buf_tx->cmd, reg + first_byte); |
|
err = spi_sync(spi, &msg); |
|
if (err) |
|
return err; |
|
|
|
memcpy(&orig_le32, buf_rx->data, len); |
|
} |
|
|
|
mask_le32 = cpu_to_le32(mask >> BITS_PER_BYTE * first_byte); |
|
val_le32 = cpu_to_le32(val >> BITS_PER_BYTE * first_byte); |
|
|
|
tmp_le32 = orig_le32 & ~mask_le32; |
|
tmp_le32 |= val_le32 & mask_le32; |
|
|
|
mcp251xfd_spi_cmd_write_nocrc(&buf_tx->cmd, reg + first_byte); |
|
memcpy(buf_tx->data, &tmp_le32, len); |
|
|
|
return spi_write(spi, buf_tx, sizeof(buf_tx->cmd) + len); |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_nocrc_read(void *context, |
|
const void *reg, size_t reg_len, |
|
void *val_buf, size_t val_len) |
|
{ |
|
struct spi_device *spi = context; |
|
struct mcp251xfd_priv *priv = spi_get_drvdata(spi); |
|
struct mcp251xfd_map_buf_nocrc *buf_rx = priv->map_buf_nocrc_rx; |
|
struct mcp251xfd_map_buf_nocrc *buf_tx = priv->map_buf_nocrc_tx; |
|
struct spi_transfer xfer[2] = { }; |
|
struct spi_message msg; |
|
int err; |
|
|
|
BUILD_BUG_ON(sizeof(buf_rx->cmd) != sizeof(__be16)); |
|
BUILD_BUG_ON(sizeof(buf_tx->cmd) != sizeof(__be16)); |
|
|
|
if (IS_ENABLED(CONFIG_CAN_MCP251XFD_SANITY) && |
|
reg_len != sizeof(buf_tx->cmd.cmd)) |
|
return -EINVAL; |
|
|
|
spi_message_init(&msg); |
|
spi_message_add_tail(&xfer[0], &msg); |
|
|
|
if (priv->devtype_data.quirks & MCP251XFD_QUIRK_HALF_DUPLEX) { |
|
xfer[0].tx_buf = reg; |
|
xfer[0].len = sizeof(buf_tx->cmd); |
|
|
|
xfer[1].rx_buf = val_buf; |
|
xfer[1].len = val_len; |
|
spi_message_add_tail(&xfer[1], &msg); |
|
} else { |
|
xfer[0].tx_buf = buf_tx; |
|
xfer[0].rx_buf = buf_rx; |
|
xfer[0].len = sizeof(buf_tx->cmd) + val_len; |
|
|
|
memcpy(&buf_tx->cmd, reg, sizeof(buf_tx->cmd)); |
|
if (MCP251XFD_SANITIZE_SPI) |
|
memset(buf_tx->data, 0x0, val_len); |
|
} |
|
|
|
err = spi_sync(spi, &msg); |
|
if (err) |
|
return err; |
|
|
|
if (!(priv->devtype_data.quirks & MCP251XFD_QUIRK_HALF_DUPLEX)) |
|
memcpy(val_buf, buf_rx->data, val_len); |
|
|
|
return 0; |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_crc_gather_write(void *context, |
|
const void *reg_p, size_t reg_len, |
|
const void *val, size_t val_len) |
|
{ |
|
struct spi_device *spi = context; |
|
struct mcp251xfd_priv *priv = spi_get_drvdata(spi); |
|
struct mcp251xfd_map_buf_crc *buf_tx = priv->map_buf_crc_tx; |
|
struct spi_transfer xfer[] = { |
|
{ |
|
.tx_buf = buf_tx, |
|
.len = sizeof(buf_tx->cmd) + val_len + |
|
sizeof(buf_tx->crc), |
|
}, |
|
}; |
|
u16 reg = *(u16 *)reg_p; |
|
u16 crc; |
|
|
|
BUILD_BUG_ON(sizeof(buf_tx->cmd) != sizeof(__be16) + sizeof(u8)); |
|
|
|
if (IS_ENABLED(CONFIG_CAN_MCP251XFD_SANITY) && |
|
reg_len != sizeof(buf_tx->cmd.cmd) + |
|
mcp251xfd_regmap_crc.pad_bits / BITS_PER_BYTE) |
|
return -EINVAL; |
|
|
|
mcp251xfd_spi_cmd_write_crc(&buf_tx->cmd, reg, val_len); |
|
memcpy(buf_tx->data, val, val_len); |
|
|
|
crc = mcp251xfd_crc16_compute(buf_tx, sizeof(buf_tx->cmd) + val_len); |
|
put_unaligned_be16(crc, buf_tx->data + val_len); |
|
|
|
return spi_sync_transfer(spi, xfer, ARRAY_SIZE(xfer)); |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_crc_write(void *context, |
|
const void *data, size_t count) |
|
{ |
|
const size_t data_offset = sizeof(__be16) + |
|
mcp251xfd_regmap_crc.pad_bits / BITS_PER_BYTE; |
|
|
|
return mcp251xfd_regmap_crc_gather_write(context, |
|
data, data_offset, |
|
data + data_offset, |
|
count - data_offset); |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_crc_read_one(struct mcp251xfd_priv *priv, |
|
struct spi_message *msg, unsigned int data_len) |
|
{ |
|
const struct mcp251xfd_map_buf_crc *buf_rx = priv->map_buf_crc_rx; |
|
const struct mcp251xfd_map_buf_crc *buf_tx = priv->map_buf_crc_tx; |
|
u16 crc_received, crc_calculated; |
|
int err; |
|
|
|
BUILD_BUG_ON(sizeof(buf_rx->cmd) != sizeof(__be16) + sizeof(u8)); |
|
BUILD_BUG_ON(sizeof(buf_tx->cmd) != sizeof(__be16) + sizeof(u8)); |
|
|
|
err = spi_sync(priv->spi, msg); |
|
if (err) |
|
return err; |
|
|
|
crc_received = get_unaligned_be16(buf_rx->data + data_len); |
|
crc_calculated = mcp251xfd_crc16_compute2(&buf_tx->cmd, |
|
sizeof(buf_tx->cmd), |
|
buf_rx->data, |
|
data_len); |
|
if (crc_received != crc_calculated) |
|
return -EBADMSG; |
|
|
|
return 0; |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_crc_read(void *context, |
|
const void *reg_p, size_t reg_len, |
|
void *val_buf, size_t val_len) |
|
{ |
|
struct spi_device *spi = context; |
|
struct mcp251xfd_priv *priv = spi_get_drvdata(spi); |
|
struct mcp251xfd_map_buf_crc *buf_rx = priv->map_buf_crc_rx; |
|
struct mcp251xfd_map_buf_crc *buf_tx = priv->map_buf_crc_tx; |
|
struct spi_transfer xfer[2] = { }; |
|
struct spi_message msg; |
|
u16 reg = *(u16 *)reg_p; |
|
int i, err; |
|
|
|
BUILD_BUG_ON(sizeof(buf_rx->cmd) != sizeof(__be16) + sizeof(u8)); |
|
BUILD_BUG_ON(sizeof(buf_tx->cmd) != sizeof(__be16) + sizeof(u8)); |
|
|
|
if (IS_ENABLED(CONFIG_CAN_MCP251XFD_SANITY) && |
|
reg_len != sizeof(buf_tx->cmd.cmd) + |
|
mcp251xfd_regmap_crc.pad_bits / BITS_PER_BYTE) |
|
return -EINVAL; |
|
|
|
spi_message_init(&msg); |
|
spi_message_add_tail(&xfer[0], &msg); |
|
|
|
if (priv->devtype_data.quirks & MCP251XFD_QUIRK_HALF_DUPLEX) { |
|
xfer[0].tx_buf = buf_tx; |
|
xfer[0].len = sizeof(buf_tx->cmd); |
|
|
|
xfer[1].rx_buf = buf_rx->data; |
|
xfer[1].len = val_len + sizeof(buf_tx->crc); |
|
spi_message_add_tail(&xfer[1], &msg); |
|
} else { |
|
xfer[0].tx_buf = buf_tx; |
|
xfer[0].rx_buf = buf_rx; |
|
xfer[0].len = sizeof(buf_tx->cmd) + val_len + |
|
sizeof(buf_tx->crc); |
|
|
|
if (MCP251XFD_SANITIZE_SPI) |
|
memset(buf_tx->data, 0x0, val_len + |
|
sizeof(buf_tx->crc)); |
|
} |
|
|
|
mcp251xfd_spi_cmd_read_crc(&buf_tx->cmd, reg, val_len); |
|
|
|
for (i = 0; i < MCP251XFD_READ_CRC_RETRIES_MAX; i++) { |
|
err = mcp251xfd_regmap_crc_read_one(priv, &msg, val_len); |
|
if (!err) |
|
goto out; |
|
if (err != -EBADMSG) |
|
return err; |
|
|
|
/* MCP251XFD_REG_OSC is the first ever reg we read from. |
|
* |
|
* The chip may be in deep sleep and this SPI transfer |
|
* (i.e. the assertion of the CS) will wake the chip |
|
* up. This takes about 3ms. The CRC of this transfer |
|
* is wrong. |
|
* |
|
* Or there isn't a chip at all, in this case the CRC |
|
* will be wrong, too. |
|
* |
|
* In both cases ignore the CRC and copy the read data |
|
* to the caller. It will take care of both cases. |
|
* |
|
*/ |
|
if (reg == MCP251XFD_REG_OSC) { |
|
err = 0; |
|
goto out; |
|
} |
|
|
|
netdev_info(priv->ndev, |
|
"CRC read error at address 0x%04x (length=%zd, data=%*ph, CRC=0x%04x) retrying.\n", |
|
reg, val_len, (int)val_len, buf_rx->data, |
|
get_unaligned_be16(buf_rx->data + val_len)); |
|
} |
|
|
|
if (err) { |
|
netdev_err(priv->ndev, |
|
"CRC read error at address 0x%04x (length=%zd, data=%*ph, CRC=0x%04x).\n", |
|
reg, val_len, (int)val_len, buf_rx->data, |
|
get_unaligned_be16(buf_rx->data + val_len)); |
|
|
|
return err; |
|
} |
|
out: |
|
memcpy(val_buf, buf_rx->data, val_len); |
|
|
|
return 0; |
|
} |
|
|
|
static const struct regmap_range mcp251xfd_reg_table_yes_range[] = { |
|
regmap_reg_range(0x000, 0x2ec), /* CAN FD Controller Module SFR */ |
|
regmap_reg_range(0x400, 0xbfc), /* RAM */ |
|
regmap_reg_range(0xe00, 0xe14), /* MCP2517/18FD SFR */ |
|
}; |
|
|
|
static const struct regmap_access_table mcp251xfd_reg_table = { |
|
.yes_ranges = mcp251xfd_reg_table_yes_range, |
|
.n_yes_ranges = ARRAY_SIZE(mcp251xfd_reg_table_yes_range), |
|
}; |
|
|
|
static const struct regmap_config mcp251xfd_regmap_nocrc = { |
|
.name = "nocrc", |
|
.reg_bits = 16, |
|
.reg_stride = 4, |
|
.pad_bits = 0, |
|
.val_bits = 32, |
|
.max_register = 0xffc, |
|
.wr_table = &mcp251xfd_reg_table, |
|
.rd_table = &mcp251xfd_reg_table, |
|
.cache_type = REGCACHE_NONE, |
|
.read_flag_mask = (__force unsigned long) |
|
cpu_to_be16(MCP251XFD_SPI_INSTRUCTION_READ), |
|
.write_flag_mask = (__force unsigned long) |
|
cpu_to_be16(MCP251XFD_SPI_INSTRUCTION_WRITE), |
|
}; |
|
|
|
static const struct regmap_bus mcp251xfd_bus_nocrc = { |
|
.write = mcp251xfd_regmap_nocrc_write, |
|
.gather_write = mcp251xfd_regmap_nocrc_gather_write, |
|
.reg_update_bits = mcp251xfd_regmap_nocrc_update_bits, |
|
.read = mcp251xfd_regmap_nocrc_read, |
|
.reg_format_endian_default = REGMAP_ENDIAN_BIG, |
|
.val_format_endian_default = REGMAP_ENDIAN_LITTLE, |
|
.max_raw_read = sizeof_field(struct mcp251xfd_map_buf_nocrc, data), |
|
.max_raw_write = sizeof_field(struct mcp251xfd_map_buf_nocrc, data), |
|
}; |
|
|
|
static const struct regmap_config mcp251xfd_regmap_crc = { |
|
.name = "crc", |
|
.reg_bits = 16, |
|
.reg_stride = 4, |
|
.pad_bits = 16, /* keep data bits aligned */ |
|
.val_bits = 32, |
|
.max_register = 0xffc, |
|
.wr_table = &mcp251xfd_reg_table, |
|
.rd_table = &mcp251xfd_reg_table, |
|
.cache_type = REGCACHE_NONE, |
|
}; |
|
|
|
static const struct regmap_bus mcp251xfd_bus_crc = { |
|
.write = mcp251xfd_regmap_crc_write, |
|
.gather_write = mcp251xfd_regmap_crc_gather_write, |
|
.read = mcp251xfd_regmap_crc_read, |
|
.reg_format_endian_default = REGMAP_ENDIAN_NATIVE, |
|
.val_format_endian_default = REGMAP_ENDIAN_LITTLE, |
|
.max_raw_read = sizeof_field(struct mcp251xfd_map_buf_crc, data), |
|
.max_raw_write = sizeof_field(struct mcp251xfd_map_buf_crc, data), |
|
}; |
|
|
|
static inline bool |
|
mcp251xfd_regmap_use_nocrc(struct mcp251xfd_priv *priv) |
|
{ |
|
return (!(priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_REG)) || |
|
(!(priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_RX)); |
|
} |
|
|
|
static inline bool |
|
mcp251xfd_regmap_use_crc(struct mcp251xfd_priv *priv) |
|
{ |
|
return (priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_REG) || |
|
(priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_RX); |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_init_nocrc(struct mcp251xfd_priv *priv) |
|
{ |
|
if (!priv->map_nocrc) { |
|
struct regmap *map; |
|
|
|
map = devm_regmap_init(&priv->spi->dev, &mcp251xfd_bus_nocrc, |
|
priv->spi, &mcp251xfd_regmap_nocrc); |
|
if (IS_ERR(map)) |
|
return PTR_ERR(map); |
|
|
|
priv->map_nocrc = map; |
|
} |
|
|
|
if (!priv->map_buf_nocrc_rx) { |
|
priv->map_buf_nocrc_rx = |
|
devm_kzalloc(&priv->spi->dev, |
|
sizeof(*priv->map_buf_nocrc_rx), |
|
GFP_KERNEL); |
|
if (!priv->map_buf_nocrc_rx) |
|
return -ENOMEM; |
|
} |
|
|
|
if (!priv->map_buf_nocrc_tx) { |
|
priv->map_buf_nocrc_tx = |
|
devm_kzalloc(&priv->spi->dev, |
|
sizeof(*priv->map_buf_nocrc_tx), |
|
GFP_KERNEL); |
|
if (!priv->map_buf_nocrc_tx) |
|
return -ENOMEM; |
|
} |
|
|
|
if (!(priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_REG)) |
|
priv->map_reg = priv->map_nocrc; |
|
|
|
if (!(priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_RX)) |
|
priv->map_rx = priv->map_nocrc; |
|
|
|
return 0; |
|
} |
|
|
|
static void mcp251xfd_regmap_destroy_nocrc(struct mcp251xfd_priv *priv) |
|
{ |
|
if (priv->map_buf_nocrc_rx) { |
|
devm_kfree(&priv->spi->dev, priv->map_buf_nocrc_rx); |
|
priv->map_buf_nocrc_rx = NULL; |
|
} |
|
if (priv->map_buf_nocrc_tx) { |
|
devm_kfree(&priv->spi->dev, priv->map_buf_nocrc_tx); |
|
priv->map_buf_nocrc_tx = NULL; |
|
} |
|
} |
|
|
|
static int |
|
mcp251xfd_regmap_init_crc(struct mcp251xfd_priv *priv) |
|
{ |
|
if (!priv->map_crc) { |
|
struct regmap *map; |
|
|
|
map = devm_regmap_init(&priv->spi->dev, &mcp251xfd_bus_crc, |
|
priv->spi, &mcp251xfd_regmap_crc); |
|
if (IS_ERR(map)) |
|
return PTR_ERR(map); |
|
|
|
priv->map_crc = map; |
|
} |
|
|
|
if (!priv->map_buf_crc_rx) { |
|
priv->map_buf_crc_rx = |
|
devm_kzalloc(&priv->spi->dev, |
|
sizeof(*priv->map_buf_crc_rx), |
|
GFP_KERNEL); |
|
if (!priv->map_buf_crc_rx) |
|
return -ENOMEM; |
|
} |
|
|
|
if (!priv->map_buf_crc_tx) { |
|
priv->map_buf_crc_tx = |
|
devm_kzalloc(&priv->spi->dev, |
|
sizeof(*priv->map_buf_crc_tx), |
|
GFP_KERNEL); |
|
if (!priv->map_buf_crc_tx) |
|
return -ENOMEM; |
|
} |
|
|
|
if (priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_REG) |
|
priv->map_reg = priv->map_crc; |
|
|
|
if (priv->devtype_data.quirks & MCP251XFD_QUIRK_CRC_RX) |
|
priv->map_rx = priv->map_crc; |
|
|
|
return 0; |
|
} |
|
|
|
static void mcp251xfd_regmap_destroy_crc(struct mcp251xfd_priv *priv) |
|
{ |
|
if (priv->map_buf_crc_rx) { |
|
devm_kfree(&priv->spi->dev, priv->map_buf_crc_rx); |
|
priv->map_buf_crc_rx = NULL; |
|
} |
|
if (priv->map_buf_crc_tx) { |
|
devm_kfree(&priv->spi->dev, priv->map_buf_crc_tx); |
|
priv->map_buf_crc_tx = NULL; |
|
} |
|
} |
|
|
|
int mcp251xfd_regmap_init(struct mcp251xfd_priv *priv) |
|
{ |
|
int err; |
|
|
|
if (mcp251xfd_regmap_use_nocrc(priv)) { |
|
err = mcp251xfd_regmap_init_nocrc(priv); |
|
|
|
if (err) |
|
return err; |
|
} else { |
|
mcp251xfd_regmap_destroy_nocrc(priv); |
|
} |
|
|
|
if (mcp251xfd_regmap_use_crc(priv)) { |
|
err = mcp251xfd_regmap_init_crc(priv); |
|
|
|
if (err) |
|
return err; |
|
} else { |
|
mcp251xfd_regmap_destroy_crc(priv); |
|
} |
|
|
|
return 0; |
|
}
|
|
|