Commit 9ad9a52c authored by Nicolas Pitre's avatar Nicolas Pitre Committed by Boris Brezillon

i3c/master: introduce the mipi-i3c-hci driver

This adds basic support for hardware implementing the MIPI I3C HCI
specification. This driver is currently limited by the capabilities
of the I3C subsystem, meaning things like scheduled commands,
auto-commands and NCM mode are not yet supported.

This supports version 1.0 of the MIPI I3C HCI spec, as well as the
imminent release of version 1.1. Support for draft version 2.0 of the
spec is also largely included with the caveat that future adjustments
to this code are likely as the spec is still a work in progress.

This is also lightly tested as actual hardware is still very scarce,
even for HCI v1.0. Hence the EXPERIMENTAL tag. Further contributions
to this driver are expected once vendor implementations and new I3C
devices become available.
Signed-off-by: default avatarNicolas Pitre <npitre@baylibre.com>
Signed-off-by: default avatarBoris Brezillon <boris.brezillon@collabora.com>
Link: https://lore.kernel.org/linux-i3c/20201111220510.3622216-3-nico@fluxnic.net
parent c307912d
...@@ -21,3 +21,16 @@ config DW_I3C_MASTER ...@@ -21,3 +21,16 @@ config DW_I3C_MASTER
This driver can also be built as a module. If so, the module This driver can also be built as a module. If so, the module
will be called dw-i3c-master. will be called dw-i3c-master.
config MIPI_I3C_HCI
tristate "MIPI I3C Host Controller Interface driver (EXPERIMENTAL)"
depends on I3C
help
Support for hardware following the MIPI Aliance's I3C Host Controller
Interface specification.
For details please see:
https://www.mipi.org/specifications/i3c-hci
This driver can also be built as a module. If so, the module will be
called mipi-i3c-hci.
# SPDX-License-Identifier: GPL-2.0-only # SPDX-License-Identifier: GPL-2.0-only
obj-$(CONFIG_CDNS_I3C_MASTER) += i3c-master-cdns.o obj-$(CONFIG_CDNS_I3C_MASTER) += i3c-master-cdns.o
obj-$(CONFIG_DW_I3C_MASTER) += dw-i3c-master.o obj-$(CONFIG_DW_I3C_MASTER) += dw-i3c-master.o
obj-$(CONFIG_MIPI_I3C_HCI) += mipi-i3c-hci/
# SPDX-License-Identifier: BSD-3-Clause
obj-$(CONFIG_MIPI_I3C_HCI) += mipi-i3c-hci.o
mipi-i3c-hci-y := core.o ext_caps.o pio.o dma.o \
cmd_v1.o cmd_v2.o \
dat_v1.o dct_v1.o
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* Common command/response related stuff
*/
#ifndef CMD_H
#define CMD_H
/*
* Those bits are common to all descriptor formats and
* may be manipulated by the core code.
*/
#define CMD_0_TOC W0_BIT_(31)
#define CMD_0_ROC W0_BIT_(30)
#define CMD_0_ATTR W0_MASK(2, 0)
/*
* Response Descriptor Structure
*/
#define RESP_STATUS(resp) FIELD_GET(GENMASK(31, 28), resp)
#define RESP_TID(resp) FIELD_GET(GENMASK(27, 24), resp)
#define RESP_DATA_LENGTH(resp) FIELD_GET(GENMASK(21, 0), resp)
#define RESP_ERR_FIELD GENMASK(31, 28)
enum hci_resp_err {
RESP_SUCCESS = 0x0,
RESP_ERR_CRC = 0x1,
RESP_ERR_PARITY = 0x2,
RESP_ERR_FRAME = 0x3,
RESP_ERR_ADDR_HEADER = 0x4,
RESP_ERR_BCAST_NACK_7E = 0x4,
RESP_ERR_NACK = 0x5,
RESP_ERR_OVL = 0x6,
RESP_ERR_I3C_SHORT_READ = 0x7,
RESP_ERR_HC_TERMINATED = 0x8,
RESP_ERR_I2C_WR_DATA_NACK = 0x9,
RESP_ERR_BUS_XFER_ABORTED = 0x9,
RESP_ERR_NOT_SUPPORTED = 0xa,
RESP_ERR_ABORTED_WITH_CRC = 0xb,
/* 0xc to 0xf are reserved for transfer specific errors */
};
/* TID generation (4 bits wide in all cases) */
#define hci_get_tid(bits) \
(atomic_inc_return_relaxed(&hci->next_cmd_tid) % (1U << 4))
/* This abstracts operations with our command descriptor formats */
struct hci_cmd_ops {
int (*prep_ccc)(struct i3c_hci *hci, struct hci_xfer *xfer,
u8 ccc_addr, u8 ccc_cmd, bool raw);
void (*prep_i3c_xfer)(struct i3c_hci *hci, struct i3c_dev_desc *dev,
struct hci_xfer *xfer);
void (*prep_i2c_xfer)(struct i3c_hci *hci, struct i2c_dev_desc *dev,
struct hci_xfer *xfer);
int (*perform_daa)(struct i3c_hci *hci);
};
/* Our various instances */
extern const struct hci_cmd_ops mipi_i3c_hci_cmd_v1;
extern const struct hci_cmd_ops mipi_i3c_hci_cmd_v2;
#endif
This diff is collapsed.
// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* I3C HCI v2.0 Command Descriptor Handling
*
* Note: The I3C HCI v2.0 spec is still in flux. The code here will change.
*/
#include <linux/bitfield.h>
#include <linux/i3c/master.h>
#include "hci.h"
#include "cmd.h"
#include "xfer_mode_rate.h"
/*
* Unified Data Transfer Command
*/
#define CMD_0_ATTR_U FIELD_PREP(CMD_0_ATTR, 0x4)
#define CMD_U3_HDR_TSP_ML_CTRL(v) FIELD_PREP(W3_MASK(107, 104), v)
#define CMD_U3_IDB4(v) FIELD_PREP(W3_MASK(103, 96), v)
#define CMD_U3_HDR_CMD(v) FIELD_PREP(W3_MASK(103, 96), v)
#define CMD_U2_IDB3(v) FIELD_PREP(W2_MASK( 95, 88), v)
#define CMD_U2_HDR_BT(v) FIELD_PREP(W2_MASK( 95, 88), v)
#define CMD_U2_IDB2(v) FIELD_PREP(W2_MASK( 87, 80), v)
#define CMD_U2_BT_CMD2(v) FIELD_PREP(W2_MASK( 87, 80), v)
#define CMD_U2_IDB1(v) FIELD_PREP(W2_MASK( 79, 72), v)
#define CMD_U2_BT_CMD1(v) FIELD_PREP(W2_MASK( 79, 72), v)
#define CMD_U2_IDB0(v) FIELD_PREP(W2_MASK( 71, 64), v)
#define CMD_U2_BT_CMD0(v) FIELD_PREP(W2_MASK( 71, 64), v)
#define CMD_U1_ERR_HANDLING(v) FIELD_PREP(W1_MASK( 63, 62), v)
#define CMD_U1_ADD_FUNC(v) FIELD_PREP(W1_MASK( 61, 56), v)
#define CMD_U1_COMBO_XFER W1_BIT_( 55)
#define CMD_U1_DATA_LENGTH(v) FIELD_PREP(W1_MASK( 53, 32), v)
#define CMD_U0_TOC W0_BIT_( 31)
#define CMD_U0_ROC W0_BIT_( 30)
#define CMD_U0_MAY_YIELD W0_BIT_( 29)
#define CMD_U0_NACK_RCNT(v) FIELD_PREP(W0_MASK( 28, 27), v)
#define CMD_U0_IDB_COUNT(v) FIELD_PREP(W0_MASK( 26, 24), v)
#define CMD_U0_MODE_INDEX(v) FIELD_PREP(W0_MASK( 22, 18), v)
#define CMD_U0_XFER_RATE(v) FIELD_PREP(W0_MASK( 17, 15), v)
#define CMD_U0_DEV_ADDRESS(v) FIELD_PREP(W0_MASK( 14, 8), v)
#define CMD_U0_RnW W0_BIT_( 7)
#define CMD_U0_TID(v) FIELD_PREP(W0_MASK( 6, 3), v)
/*
* Address Assignment Command
*/
#define CMD_0_ATTR_A FIELD_PREP(CMD_0_ATTR, 0x2)
#define CMD_A1_DATA_LENGTH(v) FIELD_PREP(W1_MASK( 53, 32), v)
#define CMD_A0_TOC W0_BIT_( 31)
#define CMD_A0_ROC W0_BIT_( 30)
#define CMD_A0_XFER_RATE(v) FIELD_PREP(W0_MASK( 17, 15), v)
#define CMD_A0_ASSIGN_ADDRESS(v) FIELD_PREP(W0_MASK( 14, 8), v)
#define CMD_A0_TID(v) FIELD_PREP(W0_MASK( 6, 3), v)
static unsigned int get_i3c_rate_idx(struct i3c_hci *hci)
{
struct i3c_bus *bus = i3c_master_get_bus(&hci->master);
if (bus->scl_rate.i3c >= 12000000)
return XFERRATE_I3C_SDR0;
if (bus->scl_rate.i3c > 8000000)
return XFERRATE_I3C_SDR1;
if (bus->scl_rate.i3c > 6000000)
return XFERRATE_I3C_SDR2;
if (bus->scl_rate.i3c > 4000000)
return XFERRATE_I3C_SDR3;
if (bus->scl_rate.i3c > 2000000)
return XFERRATE_I3C_SDR4;
return XFERRATE_I3C_SDR_FM_FMP;
}
static unsigned int get_i2c_rate_idx(struct i3c_hci *hci)
{
struct i3c_bus *bus = i3c_master_get_bus(&hci->master);
if (bus->scl_rate.i2c >= 1000000)
return XFERRATE_I2C_FMP;
return XFERRATE_I2C_FM;
}
static void hci_cmd_v2_prep_private_xfer(struct i3c_hci *hci,
struct hci_xfer *xfer,
u8 addr, unsigned int mode,
unsigned int rate)
{
u8 *data = xfer->data;
unsigned int data_len = xfer->data_len;
bool rnw = xfer->rnw;
xfer->cmd_tid = hci_get_tid();
if (!rnw && data_len <= 5) {
xfer->cmd_desc[0] =
CMD_0_ATTR_U |
CMD_U0_TID(xfer->cmd_tid) |
CMD_U0_DEV_ADDRESS(addr) |
CMD_U0_XFER_RATE(rate) |
CMD_U0_MODE_INDEX(mode) |
CMD_U0_IDB_COUNT(data_len);
xfer->cmd_desc[1] =
CMD_U1_DATA_LENGTH(0);
xfer->cmd_desc[2] = 0;
xfer->cmd_desc[3] = 0;
switch (data_len) {
case 5:
xfer->cmd_desc[3] |= CMD_U3_IDB4(data[4]);
fallthrough;
case 4:
xfer->cmd_desc[2] |= CMD_U2_IDB3(data[3]);
fallthrough;
case 3:
xfer->cmd_desc[2] |= CMD_U2_IDB2(data[2]);
fallthrough;
case 2:
xfer->cmd_desc[2] |= CMD_U2_IDB1(data[1]);
fallthrough;
case 1:
xfer->cmd_desc[2] |= CMD_U2_IDB0(data[0]);
fallthrough;
case 0:
break;
}
/* we consumed all the data with the cmd descriptor */
xfer->data = NULL;
} else {
xfer->cmd_desc[0] =
CMD_0_ATTR_U |
CMD_U0_TID(xfer->cmd_tid) |
(rnw ? CMD_U0_RnW : 0) |
CMD_U0_DEV_ADDRESS(addr) |
CMD_U0_XFER_RATE(rate) |
CMD_U0_MODE_INDEX(mode);
xfer->cmd_desc[1] =
CMD_U1_DATA_LENGTH(data_len);
xfer->cmd_desc[2] = 0;
xfer->cmd_desc[3] = 0;
}
}
static int hci_cmd_v2_prep_ccc(struct i3c_hci *hci, struct hci_xfer *xfer,
u8 ccc_addr, u8 ccc_cmd, bool raw)
{
unsigned int mode = XFERMODE_IDX_I3C_SDR;
unsigned int rate = get_i3c_rate_idx(hci);
u8 *data = xfer->data;
unsigned int data_len = xfer->data_len;
bool rnw = xfer->rnw;
if (raw && ccc_addr != I3C_BROADCAST_ADDR) {
hci_cmd_v2_prep_private_xfer(hci, xfer, ccc_addr, mode, rate);
return 0;
}
xfer->cmd_tid = hci_get_tid();
if (!rnw && data_len <= 4) {
xfer->cmd_desc[0] =
CMD_0_ATTR_U |
CMD_U0_TID(xfer->cmd_tid) |
CMD_U0_DEV_ADDRESS(ccc_addr) |
CMD_U0_XFER_RATE(rate) |
CMD_U0_MODE_INDEX(mode) |
CMD_U0_IDB_COUNT(data_len + (!raw ? 0 : 1));
xfer->cmd_desc[1] =
CMD_U1_DATA_LENGTH(0);
xfer->cmd_desc[2] =
CMD_U2_IDB0(ccc_cmd);
xfer->cmd_desc[3] = 0;
switch (data_len) {
case 4:
xfer->cmd_desc[3] |= CMD_U3_IDB4(data[3]);
fallthrough;
case 3:
xfer->cmd_desc[2] |= CMD_U2_IDB3(data[2]);
fallthrough;
case 2:
xfer->cmd_desc[2] |= CMD_U2_IDB2(data[1]);
fallthrough;
case 1:
xfer->cmd_desc[2] |= CMD_U2_IDB1(data[0]);
fallthrough;
case 0:
break;
}
/* we consumed all the data with the cmd descriptor */
xfer->data = NULL;
} else {
xfer->cmd_desc[0] =
CMD_0_ATTR_U |
CMD_U0_TID(xfer->cmd_tid) |
(rnw ? CMD_U0_RnW : 0) |
CMD_U0_DEV_ADDRESS(ccc_addr) |
CMD_U0_XFER_RATE(rate) |
CMD_U0_MODE_INDEX(mode) |
CMD_U0_IDB_COUNT(!raw ? 0 : 1);
xfer->cmd_desc[1] =
CMD_U1_DATA_LENGTH(data_len);
xfer->cmd_desc[2] =
CMD_U2_IDB0(ccc_cmd);
xfer->cmd_desc[3] = 0;
}
return 0;
}
static void hci_cmd_v2_prep_i3c_xfer(struct i3c_hci *hci,
struct i3c_dev_desc *dev,
struct hci_xfer *xfer)
{
unsigned int mode = XFERMODE_IDX_I3C_SDR;
unsigned int rate = get_i3c_rate_idx(hci);
u8 addr = dev->info.dyn_addr;
hci_cmd_v2_prep_private_xfer(hci, xfer, addr, mode, rate);
}
static void hci_cmd_v2_prep_i2c_xfer(struct i3c_hci *hci,
struct i2c_dev_desc *dev,
struct hci_xfer *xfer)
{
unsigned int mode = XFERMODE_IDX_I2C;
unsigned int rate = get_i2c_rate_idx(hci);
u8 addr = dev->addr;
hci_cmd_v2_prep_private_xfer(hci, xfer, addr, mode, rate);
}
static int hci_cmd_v2_daa(struct i3c_hci *hci)
{
struct hci_xfer *xfer;
int ret;
u8 next_addr = 0;
u32 device_id[2];
u64 pid;
unsigned int dcr, bcr;
DECLARE_COMPLETION_ONSTACK(done);
xfer = hci_alloc_xfer(2);
if (!xfer)
return -ENOMEM;
xfer[0].data = &device_id;
xfer[0].data_len = 8;
xfer[0].rnw = true;
xfer[0].cmd_desc[1] = CMD_A1_DATA_LENGTH(8);
xfer[1].completion = &done;
for (;;) {
ret = i3c_master_get_free_addr(&hci->master, next_addr);
if (ret < 0)
break;
next_addr = ret;
DBG("next_addr = 0x%02x", next_addr);
xfer[0].cmd_tid = hci_get_tid();
xfer[0].cmd_desc[0] =
CMD_0_ATTR_A |
CMD_A0_TID(xfer[0].cmd_tid) |
CMD_A0_ROC;
xfer[1].cmd_tid = hci_get_tid();
xfer[1].cmd_desc[0] =
CMD_0_ATTR_A |
CMD_A0_TID(xfer[1].cmd_tid) |
CMD_A0_ASSIGN_ADDRESS(next_addr) |
CMD_A0_ROC |
CMD_A0_TOC;
hci->io->queue_xfer(hci, xfer, 2);
if (!wait_for_completion_timeout(&done, HZ) &&
hci->io->dequeue_xfer(hci, xfer, 2)) {
ret = -ETIME;
break;
}
if (RESP_STATUS(xfer[0].response) != RESP_SUCCESS) {
ret = 0; /* no more devices to be assigned */
break;
}
if (RESP_STATUS(xfer[1].response) != RESP_SUCCESS) {
ret = -EIO;
break;
}
pid = FIELD_GET(W1_MASK(47, 32), device_id[1]);
pid = (pid << 32) | device_id[0];
bcr = FIELD_GET(W1_MASK(55, 48), device_id[1]);
dcr = FIELD_GET(W1_MASK(63, 56), device_id[1]);
DBG("assigned address %#x to device PID=0x%llx DCR=%#x BCR=%#x",
next_addr, pid, dcr, bcr);
/*
* TODO: Extend the subsystem layer to allow for registering
* new device and provide BCR/DCR/PID at the same time.
*/
ret = i3c_master_add_i3c_dev_locked(&hci->master, next_addr);
if (ret)
break;
}
hci_free_xfer(xfer, 2);
return ret;
}
const struct hci_cmd_ops mipi_i3c_hci_cmd_v2 = {
.prep_ccc = hci_cmd_v2_prep_ccc,
.prep_i3c_xfer = hci_cmd_v2_prep_i3c_xfer,
.prep_i2c_xfer = hci_cmd_v2_prep_i2c_xfer,
.perform_daa = hci_cmd_v2_daa,
};
This diff is collapsed.
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* Common DAT related stuff
*/
#ifndef DAT_H
#define DAT_H
/* Global DAT flags */
#define DAT_0_I2C_DEVICE W0_BIT_(31)
#define DAT_0_SIR_REJECT W0_BIT_(13)
#define DAT_0_IBI_PAYLOAD W0_BIT_(12)
struct hci_dat_ops {
int (*init)(struct i3c_hci *hci);
void (*cleanup)(struct i3c_hci *hci);
int (*alloc_entry)(struct i3c_hci *hci);
void (*free_entry)(struct i3c_hci *hci, unsigned int dat_idx);
void (*set_dynamic_addr)(struct i3c_hci *hci, unsigned int dat_idx, u8 addr);
void (*set_static_addr)(struct i3c_hci *hci, unsigned int dat_idx, u8 addr);
void (*set_flags)(struct i3c_hci *hci, unsigned int dat_idx, u32 w0, u32 w1);
void (*clear_flags)(struct i3c_hci *hci, unsigned int dat_idx, u32 w0, u32 w1);
int (*get_index)(struct i3c_hci *hci, u8 address);
};
extern const struct hci_dat_ops mipi_i3c_hci_dat_v1;
#endif
// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*/
#include <linux/bitfield.h>
#include <linux/bitmap.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/i3c/master.h>
#include <linux/io.h>
#include "hci.h"
#include "dat.h"
/*
* Device Address Table Structure
*/
#define DAT_1_AUTOCMD_HDR_CODE W1_MASK(58, 51)
#define DAT_1_AUTOCMD_MODE W1_MASK(50, 48)
#define DAT_1_AUTOCMD_VALUE W1_MASK(47, 40)
#define DAT_1_AUTOCMD_MASK W1_MASK(39, 32)
/* DAT_0_I2C_DEVICE W0_BIT_(31) */
#define DAT_0_DEV_NACK_RETRY_CNT W0_MASK(30, 29)
#define DAT_0_RING_ID W0_MASK(28, 26)
#define DAT_0_DYNADDR_PARITY W0_BIT_(23)
#define DAT_0_DYNAMIC_ADDRESS W0_MASK(22, 16)
#define DAT_0_TS W0_BIT_(15)
#define DAT_0_MR_REJECT W0_BIT_(14)
/* DAT_0_SIR_REJECT W0_BIT_(13) */
/* DAT_0_IBI_PAYLOAD W0_BIT_(12) */
#define DAT_0_STATIC_ADDRESS W0_MASK(6, 0)
#define dat_w0_read(i) readl(hci->DAT_regs + (i) * 8)
#define dat_w1_read(i) readl(hci->DAT_regs + (i) * 8 + 4)
#define dat_w0_write(i, v) writel(v, hci->DAT_regs + (i) * 8)
#define dat_w1_write(i, v) writel(v, hci->DAT_regs + (i) * 8 + 4)
static inline bool dynaddr_parity(unsigned int addr)
{
addr |= 1 << 7;
addr += addr >> 4;
addr += addr >> 2;
addr += addr >> 1;
return (addr & 1);
}
static int hci_dat_v1_init(struct i3c_hci *hci)
{
unsigned int dat_idx;
if (!hci->DAT_regs) {
dev_err(&hci->master.dev,
"only DAT in register space is supported at the moment\n");
return -EOPNOTSUPP;
}
if (hci->DAT_entry_size != 8) {
dev_err(&hci->master.dev,
"only 8-bytes DAT entries are supported at the moment\n");
return -EOPNOTSUPP;
}
/* use a bitmap for faster free slot search */
hci->DAT_data = bitmap_zalloc(hci->DAT_entries, GFP_KERNEL);
if (!hci->DAT_data)
return -ENOMEM;
/* clear them */
for (dat_idx = 0; dat_idx < hci->DAT_entries; dat_idx++) {
dat_w0_write(dat_idx, 0);
dat_w1_write(dat_idx, 0);
}
return 0;
}
static void hci_dat_v1_cleanup(struct i3c_hci *hci)
{
bitmap_free(hci->DAT_data);
hci->DAT_data = NULL;
}
static int hci_dat_v1_alloc_entry(struct i3c_hci *hci)
{
unsigned int dat_idx;
dat_idx = find_first_zero_bit(hci->DAT_data, hci->DAT_entries);
if (dat_idx >= hci->DAT_entries)
return -ENOENT;
__set_bit(dat_idx, hci->DAT_data);
/* default flags */
dat_w0_write(dat_idx, DAT_0_SIR_REJECT | DAT_0_MR_REJECT);
return dat_idx;
}
static void hci_dat_v1_free_entry(struct i3c_hci *hci, unsigned int dat_idx)
{
dat_w0_write(dat_idx, 0);
dat_w1_write(dat_idx, 0);
__clear_bit(dat_idx, hci->DAT_data);
}
static void hci_dat_v1_set_dynamic_addr(struct i3c_hci *hci,
unsigned int dat_idx, u8 address)
{
u32 dat_w0;
dat_w0 = dat_w0_read(dat_idx);
dat_w0 &= ~(DAT_0_DYNAMIC_ADDRESS | DAT_0_DYNADDR_PARITY);
dat_w0 |= FIELD_PREP(DAT_0_DYNAMIC_ADDRESS, address) |
(dynaddr_parity(address) ? DAT_0_DYNADDR_PARITY : 0);
dat_w0_write(dat_idx, dat_w0);
}
static void hci_dat_v1_set_static_addr(struct i3c_hci *hci,
unsigned int dat_idx, u8 address)
{
u32 dat_w0;
dat_w0 = dat_w0_read(dat_idx);
dat_w0 &= ~DAT_0_STATIC_ADDRESS;
dat_w0 |= FIELD_PREP(DAT_0_STATIC_ADDRESS, address);
dat_w0_write(dat_idx, dat_w0);
}
static void hci_dat_v1_set_flags(struct i3c_hci *hci, unsigned int dat_idx,
u32 w0_flags, u32 w1_flags)
{
u32 dat_w0, dat_w1;
dat_w0 = dat_w0_read(dat_idx);
dat_w1 = dat_w1_read(dat_idx);
dat_w0 |= w0_flags;
dat_w1 |= w1_flags;
dat_w0_write(dat_idx, dat_w0);
dat_w1_write(dat_idx, dat_w1);
}
static void hci_dat_v1_clear_flags(struct i3c_hci *hci, unsigned int dat_idx,
u32 w0_flags, u32 w1_flags)
{
u32 dat_w0, dat_w1;
dat_w0 = dat_w0_read(dat_idx);
dat_w1 = dat_w1_read(dat_idx);
dat_w0 &= ~w0_flags;
dat_w1 &= ~w1_flags;
dat_w0_write(dat_idx, dat_w0);
dat_w1_write(dat_idx, dat_w1);
}
static int hci_dat_v1_get_index(struct i3c_hci *hci, u8 dev_addr)
{
unsigned int dat_idx;
u32 dat_w0;
for (dat_idx = find_first_bit(hci->DAT_data, hci->DAT_entries);
dat_idx < hci->DAT_entries;
dat_idx = find_next_bit(hci->DAT_data, hci->DAT_entries, dat_idx)) {
dat_w0 = dat_w0_read(dat_idx);
if (FIELD_GET(DAT_0_DYNAMIC_ADDRESS, dat_w0) == dev_addr)
return dat_idx;
}
return -ENODEV;
}
const struct hci_dat_ops mipi_i3c_hci_dat_v1 = {
.init = hci_dat_v1_init,
.cleanup = hci_dat_v1_cleanup,
.alloc_entry = hci_dat_v1_alloc_entry,
.free_entry = hci_dat_v1_free_entry,
.set_dynamic_addr = hci_dat_v1_set_dynamic_addr,
.set_static_addr = hci_dat_v1_set_static_addr,
.set_flags = hci_dat_v1_set_flags,
.clear_flags = hci_dat_v1_clear_flags,
.get_index = hci_dat_v1_get_index,
};
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* Common DCT related stuff
*/
#ifndef DCT_H
#define DCT_H
void i3c_hci_dct_get_val(struct i3c_hci *hci, unsigned int dct_idx,
u64 *pid, unsigned int *dcr, unsigned int *bcr);
#endif
// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*/
#include <linux/device.h>
#include <linux/bitfield.h>
#include <linux/i3c/master.h>
#include <linux/io.h>
#include "hci.h"
#include "dct.h"
/*
* Device Characteristic Table
*/
void i3c_hci_dct_get_val(struct i3c_hci *hci, unsigned int dct_idx,
u64 *pid, unsigned int *dcr, unsigned int *bcr)
{
void __iomem *reg = hci->DCT_regs + dct_idx * 4 * 4;
u32 dct_entry_data[4];
unsigned int i;
for (i = 0; i < 4; i++) {
dct_entry_data[i] = readl(reg);
reg += 4;
}
*pid = ((u64)dct_entry_data[0]) << (47 - 32 + 1) |
FIELD_GET(W1_MASK(47, 32), dct_entry_data[1]);
*dcr = FIELD_GET(W2_MASK(71, 64), dct_entry_data[2]);
*bcr = FIELD_GET(W2_MASK(79, 72), dct_entry_data[2]);
}
This diff is collapsed.
// SPDX-License-Identifier: BSD-3-Clause
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*/
#include <linux/bitfield.h>
#include <linux/device.h>
#include <linux/errno.h>
#include <linux/i3c/master.h>
#include <linux/kernel.h>
#include <linux/io.h>
#include "hci.h"
#include "ext_caps.h"
#include "xfer_mode_rate.h"
/* Extended Capability Header */
#define CAP_HEADER_LENGTH GENMASK(23, 8)
#define CAP_HEADER_ID GENMASK(7, 0)
static int hci_extcap_hardware_id(struct i3c_hci *hci, void __iomem *base)
{
hci->vendor_mipi_id = readl(base + 0x04);
hci->vendor_version_id = readl(base + 0x08);
hci->vendor_product_id = readl(base + 0x0c);
dev_info(&hci->master.dev, "vendor MIPI ID: %#x\n", hci->vendor_mipi_id);
dev_info(&hci->master.dev, "vendor version ID: %#x\n", hci->vendor_version_id);
dev_info(&hci->master.dev, "vendor product ID: %#x\n", hci->vendor_product_id);
/* ought to go in a table if this grows too much */
switch (hci->vendor_mipi_id) {
case MIPI_VENDOR_NXP:
hci->quirks |= HCI_QUIRK_RAW_CCC;
DBG("raw CCC quirks set");
break;
}
return 0;
}
static int hci_extcap_master_config(struct i3c_hci *hci, void __iomem *base)
{
u32 master_config = readl(base + 0x04);
unsigned int operation_mode = FIELD_GET(GENMASK(5, 4), master_config);
static const char * const functionality[] = {
"(unknown)", "master only", "target only",
"primary/secondary master" };
dev_info(&hci->master.dev, "operation mode: %s\n", functionality[operation_mode]);
if (operation_mode & 0x1)
return 0;
dev_err(&hci->master.dev, "only master mode is currently supported\n");
return -EOPNOTSUPP;
}
static int hci_extcap_multi_bus(struct i3c_hci *hci, void __iomem *base)
{
u32 bus_instance = readl(base + 0x04);
unsigned int count = FIELD_GET(GENMASK(3, 0), bus_instance);
dev_info(&hci->master.dev, "%d bus instances\n", count);
return 0;
}
static int hci_extcap_xfer_modes(struct i3c_hci *hci, void __iomem *base)
{
u32 header = readl(base);
u32 entries = FIELD_GET(CAP_HEADER_LENGTH, header) - 1;
unsigned int index;
dev_info(&hci->master.dev, "transfer mode table has %d entries\n",
entries);
base += 4; /* skip header */
for (index = 0; index < entries; index++) {
u32 mode_entry = readl(base);
DBG("mode %d: 0x%08x", index, mode_entry);
/* TODO: will be needed when I3C core does more than SDR */
base += 4;
}
return 0;
}
static int hci_extcap_xfer_rates(struct i3c_hci *hci, void __iomem *base)
{
u32 header = readl(base);
u32 entries = FIELD_GET(CAP_HEADER_LENGTH, header) - 1;
u32 rate_entry;
unsigned int index, rate, rate_id, mode_id;
base += 4; /* skip header */
dev_info(&hci->master.dev, "available data rates:\n");
for (index = 0; index < entries; index++) {
rate_entry = readl(base);
DBG("entry %d: 0x%08x", index, rate_entry);
rate = FIELD_GET(XFERRATE_ACTUAL_RATE_KHZ, rate_entry);
rate_id = FIELD_GET(XFERRATE_RATE_ID, rate_entry);
mode_id = FIELD_GET(XFERRATE_MODE_ID, rate_entry);
dev_info(&hci->master.dev, "rate %d for %s = %d kHz\n",
rate_id,
mode_id == XFERRATE_MODE_I3C ? "I3C" :
mode_id == XFERRATE_MODE_I2C ? "I2C" :
"unknown mode",
rate);
base += 4;
}
return 0;
}
static int hci_extcap_auto_command(struct i3c_hci *hci, void __iomem *base)
{
u32 autocmd_ext_caps = readl(base + 0x04);
unsigned int max_count = FIELD_GET(GENMASK(3, 0), autocmd_ext_caps);
u32 autocmd_ext_config = readl(base + 0x08);
unsigned int count = FIELD_GET(GENMASK(3, 0), autocmd_ext_config);
dev_info(&hci->master.dev, "%d/%d active auto-command entries\n",
count, max_count);
/* remember auto-command register location for later use */
hci->AUTOCMD_regs = base;
return 0;
}
static int hci_extcap_debug(struct i3c_hci *hci, void __iomem *base)
{
dev_info(&hci->master.dev, "debug registers present\n");
hci->DEBUG_regs = base;
return 0;
}
static int hci_extcap_scheduled_cmd(struct i3c_hci *hci, void __iomem *base)
{
dev_info(&hci->master.dev, "scheduled commands available\n");
/* hci->schedcmd_regs = base; */
return 0;
}
static int hci_extcap_non_curr_master(struct i3c_hci *hci, void __iomem *base)
{
dev_info(&hci->master.dev, "Non-Current Master support available\n");
/* hci->NCM_regs = base; */
return 0;
}
static int hci_extcap_ccc_resp_conf(struct i3c_hci *hci, void __iomem *base)
{
dev_info(&hci->master.dev, "CCC Response Configuration available\n");
return 0;
}
static int hci_extcap_global_DAT(struct i3c_hci *hci, void __iomem *base)
{
dev_info(&hci->master.dev, "Global DAT available\n");
return 0;
}
static int hci_extcap_multilane(struct i3c_hci *hci, void __iomem *base)
{
dev_info(&hci->master.dev, "Master Multi-Lane support available\n");
return 0;
}
static int hci_extcap_ncm_multilane(struct i3c_hci *hci, void __iomem *base)
{
dev_info(&hci->master.dev, "NCM Multi-Lane support available\n");
return 0;
}
struct hci_ext_caps {
u8 id;
u16 min_length;
int (*parser)(struct i3c_hci *hci, void __iomem *base);
};
#define EXT_CAP(_id, _highest_mandatory_reg_offset, _parser) \
{ .id = (_id), .parser = (_parser), \
.min_length = (_highest_mandatory_reg_offset)/4 + 1 }
static const struct hci_ext_caps ext_capabilities[] = {
EXT_CAP(0x01, 0x0c, hci_extcap_hardware_id),
EXT_CAP(0x02, 0x04, hci_extcap_master_config),
EXT_CAP(0x03, 0x04, hci_extcap_multi_bus),
EXT_CAP(0x04, 0x24, hci_extcap_xfer_modes),
EXT_CAP(0x05, 0x08, hci_extcap_auto_command),
EXT_CAP(0x08, 0x40, hci_extcap_xfer_rates),
EXT_CAP(0x0c, 0x10, hci_extcap_debug),
EXT_CAP(0x0d, 0x0c, hci_extcap_scheduled_cmd),
EXT_CAP(0x0e, 0x80, hci_extcap_non_curr_master), /* TODO confirm size */
EXT_CAP(0x0f, 0x04, hci_extcap_ccc_resp_conf),
EXT_CAP(0x10, 0x08, hci_extcap_global_DAT),
EXT_CAP(0x9d, 0x04, hci_extcap_multilane),
EXT_CAP(0x9e, 0x04, hci_extcap_ncm_multilane),
};
static int hci_extcap_vendor_NXP(struct i3c_hci *hci, void __iomem *base)
{
hci->vendor_data = (__force void *)base;
dev_info(&hci->master.dev, "Build Date Info = %#x\n", readl(base + 1*4));
/* reset the FPGA */
writel(0xdeadbeef, base + 1*4);
return 0;
}
struct hci_ext_cap_vendor_specific {
u32 vendor;
u8 cap;
u16 min_length;
int (*parser)(struct i3c_hci *hci, void __iomem *base);
};
#define EXT_CAP_VENDOR(_vendor, _cap, _highest_mandatory_reg_offset) \
{ .vendor = (MIPI_VENDOR_##_vendor), .cap = (_cap), \
.parser = (hci_extcap_vendor_##_vendor), \
.min_length = (_highest_mandatory_reg_offset)/4 + 1 }
static const struct hci_ext_cap_vendor_specific vendor_ext_caps[] = {
EXT_CAP_VENDOR(NXP, 0xc0, 0x20),
};
static int hci_extcap_vendor_specific(struct i3c_hci *hci, void __iomem *base,
u32 cap_id, u32 cap_length)
{
const struct hci_ext_cap_vendor_specific *vendor_cap_entry;
int i;
vendor_cap_entry = NULL;
for (i = 0; i < ARRAY_SIZE(vendor_ext_caps); i++) {
if (vendor_ext_caps[i].vendor == hci->vendor_mipi_id &&
vendor_ext_caps[i].cap == cap_id) {
vendor_cap_entry = &vendor_ext_caps[i];
break;
}
}
if (!vendor_cap_entry) {
dev_notice(&hci->master.dev,
"unknown ext_cap 0x%02x for vendor 0x%02x\n",
cap_id, hci->vendor_mipi_id);
return 0;
}
if (cap_length < vendor_cap_entry->min_length) {
dev_err(&hci->master.dev,
"ext_cap 0x%02x has size %d (expecting >= %d)\n",
cap_id, cap_length, vendor_cap_entry->min_length);
return -EINVAL;
}
return vendor_cap_entry->parser(hci, base);
}
int i3c_hci_parse_ext_caps(struct i3c_hci *hci)
{
void __iomem *curr_cap = hci->EXTCAPS_regs;
void __iomem *end = curr_cap + 0x1000; /* some arbitrary limit */
u32 cap_header, cap_id, cap_length;
const struct hci_ext_caps *cap_entry;
int i, err = 0;
if (!curr_cap)
return 0;
for (; !err && curr_cap < end; curr_cap += cap_length * 4) {
cap_header = readl(curr_cap);
cap_id = FIELD_GET(CAP_HEADER_ID, cap_header);
cap_length = FIELD_GET(CAP_HEADER_LENGTH, cap_header);
DBG("id=0x%02x length=%d", cap_id, cap_length);
if (!cap_length)
break;
if (curr_cap + cap_length * 4 >= end) {
dev_err(&hci->master.dev,
"ext_cap 0x%02x has size %d (too big)\n",
cap_id, cap_length);
err = -EINVAL;
break;
}
if (cap_id >= 0xc0 && cap_id <= 0xcf) {
err = hci_extcap_vendor_specific(hci, curr_cap,
cap_id, cap_length);
continue;
}
cap_entry = NULL;
for (i = 0; i < ARRAY_SIZE(ext_capabilities); i++) {
if (ext_capabilities[i].id == cap_id) {
cap_entry = &ext_capabilities[i];
break;
}
}
if (!cap_entry) {
dev_notice(&hci->master.dev,
"unknown ext_cap 0x%02x\n", cap_id);
} else if (cap_length < cap_entry->min_length) {
dev_err(&hci->master.dev,
"ext_cap 0x%02x has size %d (expecting >= %d)\n",
cap_id, cap_length, cap_entry->min_length);
err = -EINVAL;
} else {
err = cap_entry->parser(hci, curr_cap);
}
}
return err;
}
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* Extended Capability Definitions
*/
#ifndef EXTCAPS_H
#define EXTCAPS_H
/* MIPI vendor IDs */
#define MIPI_VENDOR_NXP 0x11b
int i3c_hci_parse_ext_caps(struct i3c_hci *hci);
#endif
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* Common HCI stuff
*/
#ifndef HCI_H
#define HCI_H
/* Handy logging macro to save on line length */
#define DBG(x, ...) pr_devel("%s: " x "\n", __func__, ##__VA_ARGS__)
/* 32-bit word aware bit and mask macros */
#define W0_MASK(h, l) GENMASK((h) - 0, (l) - 0)
#define W1_MASK(h, l) GENMASK((h) - 32, (l) - 32)
#define W2_MASK(h, l) GENMASK((h) - 64, (l) - 64)
#define W3_MASK(h, l) GENMASK((h) - 96, (l) - 96)
/* Same for single bit macros (trailing _ to align with W*_MASK width) */
#define W0_BIT_(x) BIT((x) - 0)
#define W1_BIT_(x) BIT((x) - 32)
#define W2_BIT_(x) BIT((x) - 64)
#define W3_BIT_(x) BIT((x) - 96)
struct hci_cmd_ops;
/* Our main structure */
struct i3c_hci {
struct i3c_master_controller master;
void __iomem *base_regs;
void __iomem *DAT_regs;
void __iomem *DCT_regs;
void __iomem *RHS_regs;
void __iomem *PIO_regs;
void __iomem *EXTCAPS_regs;
void __iomem *AUTOCMD_regs;
void __iomem *DEBUG_regs;
const struct hci_io_ops *io;
void *io_data;
const struct hci_cmd_ops *cmd;
atomic_t next_cmd_tid;
u32 caps;
unsigned int quirks;
unsigned int DAT_entries;
unsigned int DAT_entry_size;
void *DAT_data;
unsigned int DCT_entries;
unsigned int DCT_entry_size;
u8 version_major;
u8 version_minor;
u8 revision;
u32 vendor_mipi_id;
u32 vendor_version_id;
u32 vendor_product_id;
void *vendor_data;
};
/*
* Structure to represent a master initiated transfer.
* The rnw, data and data_len fields must be initialized before calling any
* hci->cmd->*() method. The cmd method will initialize cmd_desc[] and
* possibly modify (clear) the data field. Then xfer->cmd_desc[0] can
* be augmented with CMD_0_ROC and/or CMD_0_TOC.
* The completion field needs to be initialized before queueing with
* hci->io->queue_xfer(), and requires CMD_0_ROC to be set.
*/
struct hci_xfer {
u32 cmd_desc[4];
u32 response;
bool rnw;
void *data;
unsigned int data_len;
unsigned int cmd_tid;
struct completion *completion;
union {
struct {
/* PIO specific */
struct hci_xfer *next_xfer;
struct hci_xfer *next_data;
struct hci_xfer *next_resp;
unsigned int data_left;
u32 data_word_before_partial;
};
struct {
/* DMA specific */
dma_addr_t data_dma;
int ring_number;
int ring_entry;
};
};
};
static inline struct hci_xfer *hci_alloc_xfer(unsigned int n)
{
return kzalloc(sizeof(struct hci_xfer) * n, GFP_KERNEL);
}
static inline void hci_free_xfer(struct hci_xfer *xfer, unsigned int n)
{
kfree(xfer);
}
/* This abstracts PIO vs DMA operations */
struct hci_io_ops {
bool (*irq_handler)(struct i3c_hci *hci, unsigned int mask);
int (*queue_xfer)(struct i3c_hci *hci, struct hci_xfer *xfer, int n);
bool (*dequeue_xfer)(struct i3c_hci *hci, struct hci_xfer *xfer, int n);
int (*request_ibi)(struct i3c_hci *hci, struct i3c_dev_desc *dev,
const struct i3c_ibi_setup *req);
void (*free_ibi)(struct i3c_hci *hci, struct i3c_dev_desc *dev);
void (*recycle_ibi_slot)(struct i3c_hci *hci, struct i3c_dev_desc *dev,
struct i3c_ibi_slot *slot);
int (*init)(struct i3c_hci *hci);
void (*cleanup)(struct i3c_hci *hci);
};
extern const struct hci_io_ops mipi_i3c_hci_pio;
extern const struct hci_io_ops mipi_i3c_hci_dma;
/* Our per device master private data */
struct i3c_hci_dev_data {
int dat_idx;
void *ibi_data;
};
/* list of quirks */
#define HCI_QUIRK_RAW_CCC BIT(1) /* CCC framing must be explicit */
/* global functions */
void mipi_i3c_hci_resume(struct i3c_hci *hci);
void mipi_i3c_hci_pio_reset(struct i3c_hci *hci);
void mipi_i3c_hci_dct_index_reset(struct i3c_hci *hci);
#endif
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* Common IBI related stuff
*/
#ifndef IBI_H
#define IBI_H
/*
* IBI Status Descriptor bits
*/
#define IBI_STS BIT(31)
#define IBI_ERROR BIT(30)
#define IBI_STATUS_TYPE BIT(29)
#define IBI_HW_CONTEXT GENMASK(28, 26)
#define IBI_TS BIT(25)
#define IBI_LAST_STATUS BIT(24)
#define IBI_CHUNKS GENMASK(23, 16)
#define IBI_ID GENMASK(15, 8)
#define IBI_TARGET_ADDR GENMASK(15, 9)
#define IBI_TARGET_RNW BIT(8)
#define IBI_DATA_LENGTH GENMASK(7, 0)
/* handy helpers */
static inline struct i3c_dev_desc *
i3c_hci_addr_to_dev(struct i3c_hci *hci, unsigned int addr)
{
struct i3c_bus *bus = i3c_master_get_bus(&hci->master);
struct i3c_dev_desc *dev;
i3c_bus_for_each_i3cdev(bus, dev) {
if (dev->info.dyn_addr == addr)
return dev;
}
return NULL;
}
#endif
This diff is collapsed.
/* SPDX-License-Identifier: BSD-3-Clause */
/*
* Copyright (c) 2020, MIPI Alliance, Inc.
*
* Author: Nicolas Pitre <npitre@baylibre.com>
*
* Transfer Mode/Rate Table definitions as found in extended capability
* sections 0x04 and 0x08.
* This applies starting from I3C HCI v2.0.
*/
#ifndef XFER_MODE_RATE_H
#define XFER_MODE_RATE_H
/*
* Master Transfer Mode Table Fixed Indexes.
*
* Indexes 0x0 and 0x8 are mandatory. Availability for the rest must be
* obtained from the mode table in the extended capability area.
* Presence and definitions for indexes beyond these ones may vary.
*/
#define XFERMODE_IDX_I3C_SDR 0x00 /* I3C SDR Mode */
#define XFERMODE_IDX_I3C_HDR_DDR 0x01 /* I3C HDR-DDR Mode */
#define XFERMODE_IDX_I3C_HDR_T 0x02 /* I3C HDR-Ternary Mode */
#define XFERMODE_IDX_I3C_HDR_BT 0x03 /* I3C HDR-BT Mode */
#define XFERMODE_IDX_I2C 0x08 /* Legacy I2C Mode */
/*
* Transfer Mode Table Entry Bits Definitions
*/
#define XFERMODE_VALID_XFER_ADD_FUNC GENMASK(21, 16)
#define XFERMODE_ML_DATA_XFER_CODING GENMASK(15, 11)
#define XFERMODE_ML_ADDL_LANES GENMASK(10, 8)
#define XFERMODE_SUPPORTED BIT(7)
#define XFERMODE_MODE GENMASK(3, 0)
/*
* Master Data Transfer Rate Selector Values.
*
* These are the values to be used in the command descriptor XFER_RATE field
* and found in the RATE_ID field below.
* The I3C_SDR0, I3C_SDR1, I3C_SDR2, I3C_SDR3, I3C_SDR4 and I2C_FM rates
* are required, everything else is optional and discoverable in the
* Data Transfer Rate Table. Indicated are typical rates. The actual
* rates may vary slightly and are also specified in the Data Transfer
* Rate Table.
*/
#define XFERRATE_I3C_SDR0 0x00 /* 12.5 MHz */
#define XFERRATE_I3C_SDR1 0x01 /* 8 MHz */
#define XFERRATE_I3C_SDR2 0x02 /* 6 MHz */
#define XFERRATE_I3C_SDR3 0x03 /* 4 MHz */
#define XFERRATE_I3C_SDR4 0x04 /* 2 MHz */
#define XFERRATE_I3C_SDR_FM_FMP 0x05 /* 400 KHz / 1 MHz */
#define XFERRATE_I3C_SDR_USER6 0x06 /* User Defined */
#define XFERRATE_I3C_SDR_USER7 0x07 /* User Defined */
#define XFERRATE_I2C_FM 0x00 /* 400 KHz */
#define XFERRATE_I2C_FMP 0x01 /* 1 MHz */
#define XFERRATE_I2C_USER2 0x02 /* User Defined */
#define XFERRATE_I2C_USER3 0x03 /* User Defined */
#define XFERRATE_I2C_USER4 0x04 /* User Defined */
#define XFERRATE_I2C_USER5 0x05 /* User Defined */
#define XFERRATE_I2C_USER6 0x06 /* User Defined */
#define XFERRATE_I2C_USER7 0x07 /* User Defined */
/*
* Master Data Transfer Rate Table Mode ID values.
*/
#define XFERRATE_MODE_I3C 0x00
#define XFERRATE_MODE_I2C 0x08
/*
* Master Data Transfer Rate Table Entry Bits Definitions
*/
#define XFERRATE_MODE_ID GENMASK(31, 28)
#define XFERRATE_RATE_ID GENMASK(22, 20)
#define XFERRATE_ACTUAL_RATE_KHZ GENMASK(19, 0)
#endif
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment