mirror of
https://github.com/torvalds/linux.git
synced 2024-11-22 04:38:03 +00:00
580db513b4
MCTP control protocol implementations are transport binding dependent. Endpoint discovery is mandatory based on transport binding. Message timing requirements are specified in each respective transport binding specification. However, we currently have no means to get this information from MCTP links. Add a IFLA_MCTP_PHYS_BINDING netlink link attribute, which represents the transport type using the DMTF DSP0239-defined type numbers, returned as part of RTM_GETLINK data. We get an IFLA_MCTP_PHYS_BINDING attribute for each MCTP link, for example: - 0x00 (unspec) for loopback interface; - 0x01 (SMBus/I2C) for mctpi2c%d interfaces; and - 0x05 (serial) for mctpserial%d interfaces. Signed-off-by: Khang Nguyen <khangng@os.amperecomputing.com> Reviewed-by: Matt Johnston <matt@codeconstruct.com.au> Link: https://patch.msgid.link/20241105071915.821871-1-khangng@os.amperecomputing.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
635 lines
13 KiB
C
635 lines
13 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Management Component Transport Protocol (MCTP) - serial transport
|
|
* binding. This driver is an implementation of the DMTF specificiation
|
|
* "DSP0253 - Management Component Transport Protocol (MCTP) Serial Transport
|
|
* Binding", available at:
|
|
*
|
|
* https://www.dmtf.org/sites/default/files/standards/documents/DSP0253_1.0.0.pdf
|
|
*
|
|
* This driver provides DSP0253-type MCTP-over-serial transport using a Linux
|
|
* tty device, by setting the N_MCTP line discipline on the tty.
|
|
*
|
|
* Copyright (c) 2021 Code Construct
|
|
*/
|
|
|
|
#include <linux/idr.h>
|
|
#include <linux/if_arp.h>
|
|
#include <linux/module.h>
|
|
#include <linux/skbuff.h>
|
|
#include <linux/tty.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/crc-ccitt.h>
|
|
|
|
#include <linux/mctp.h>
|
|
#include <net/mctp.h>
|
|
#include <net/mctpdevice.h>
|
|
#include <net/pkt_sched.h>
|
|
|
|
#define MCTP_SERIAL_MTU 68 /* base mtu (64) + mctp header */
|
|
#define MCTP_SERIAL_FRAME_MTU (MCTP_SERIAL_MTU + 6) /* + serial framing */
|
|
|
|
#define MCTP_SERIAL_VERSION 0x1 /* DSP0253 defines a single version: 1 */
|
|
|
|
#define BUFSIZE MCTP_SERIAL_FRAME_MTU
|
|
|
|
#define BYTE_FRAME 0x7e
|
|
#define BYTE_ESC 0x7d
|
|
|
|
#define FCS_INIT 0xffff
|
|
|
|
static DEFINE_IDA(mctp_serial_ida);
|
|
|
|
enum mctp_serial_state {
|
|
STATE_IDLE,
|
|
STATE_START,
|
|
STATE_HEADER,
|
|
STATE_DATA,
|
|
STATE_ESCAPE,
|
|
STATE_TRAILER,
|
|
STATE_DONE,
|
|
STATE_ERR,
|
|
};
|
|
|
|
struct mctp_serial {
|
|
struct net_device *netdev;
|
|
struct tty_struct *tty;
|
|
|
|
int idx;
|
|
|
|
/* protects our rx & tx state machines; held during both paths */
|
|
spinlock_t lock;
|
|
|
|
struct work_struct tx_work;
|
|
enum mctp_serial_state txstate, rxstate;
|
|
u16 txfcs, rxfcs, rxfcs_rcvd;
|
|
unsigned int txlen, rxlen;
|
|
unsigned int txpos, rxpos;
|
|
u8 txbuf[BUFSIZE],
|
|
rxbuf[BUFSIZE];
|
|
};
|
|
|
|
static bool needs_escape(u8 c)
|
|
{
|
|
return c == BYTE_ESC || c == BYTE_FRAME;
|
|
}
|
|
|
|
static unsigned int next_chunk_len(struct mctp_serial *dev)
|
|
{
|
|
unsigned int i;
|
|
|
|
/* either we have no bytes to send ... */
|
|
if (dev->txpos == dev->txlen)
|
|
return 0;
|
|
|
|
/* ... or the next byte to send is an escaped byte; requiring a
|
|
* single-byte chunk...
|
|
*/
|
|
if (needs_escape(dev->txbuf[dev->txpos]))
|
|
return 1;
|
|
|
|
/* ... or we have one or more bytes up to the next escape - this chunk
|
|
* will be those non-escaped bytes, and does not include the escaped
|
|
* byte.
|
|
*/
|
|
for (i = 1; i + dev->txpos < dev->txlen; i++) {
|
|
if (needs_escape(dev->txbuf[dev->txpos + i]))
|
|
break;
|
|
}
|
|
|
|
return i;
|
|
}
|
|
|
|
static ssize_t write_chunk(struct mctp_serial *dev, u8 *buf, size_t len)
|
|
{
|
|
return dev->tty->ops->write(dev->tty, buf, len);
|
|
}
|
|
|
|
static void mctp_serial_tx_work(struct work_struct *work)
|
|
{
|
|
struct mctp_serial *dev = container_of(work, struct mctp_serial,
|
|
tx_work);
|
|
unsigned long flags;
|
|
ssize_t txlen;
|
|
unsigned int len;
|
|
u8 c, buf[3];
|
|
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
|
|
/* txstate represents the next thing to send */
|
|
switch (dev->txstate) {
|
|
case STATE_START:
|
|
dev->txpos = 0;
|
|
fallthrough;
|
|
case STATE_HEADER:
|
|
buf[0] = BYTE_FRAME;
|
|
buf[1] = MCTP_SERIAL_VERSION;
|
|
buf[2] = dev->txlen;
|
|
|
|
if (!dev->txpos)
|
|
dev->txfcs = crc_ccitt(FCS_INIT, buf + 1, 2);
|
|
|
|
txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txpos += txlen;
|
|
if (dev->txpos == 3) {
|
|
dev->txstate = STATE_DATA;
|
|
dev->txpos = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_ESCAPE:
|
|
buf[0] = dev->txbuf[dev->txpos] & ~0x20;
|
|
txlen = write_chunk(dev, buf, 1);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txpos += txlen;
|
|
if (dev->txpos == dev->txlen) {
|
|
dev->txstate = STATE_TRAILER;
|
|
dev->txpos = 0;
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case STATE_DATA:
|
|
len = next_chunk_len(dev);
|
|
if (len) {
|
|
c = dev->txbuf[dev->txpos];
|
|
if (len == 1 && needs_escape(c)) {
|
|
buf[0] = BYTE_ESC;
|
|
buf[1] = c & ~0x20;
|
|
dev->txfcs = crc_ccitt_byte(dev->txfcs, c);
|
|
txlen = write_chunk(dev, buf, 2);
|
|
if (txlen == 2)
|
|
dev->txpos++;
|
|
else if (txlen == 1)
|
|
dev->txstate = STATE_ESCAPE;
|
|
else
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
txlen = write_chunk(dev,
|
|
dev->txbuf + dev->txpos,
|
|
len);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txfcs = crc_ccitt(dev->txfcs,
|
|
dev->txbuf +
|
|
dev->txpos,
|
|
txlen);
|
|
dev->txpos += txlen;
|
|
}
|
|
}
|
|
if (dev->txstate == STATE_DATA &&
|
|
dev->txpos == dev->txlen) {
|
|
dev->txstate = STATE_TRAILER;
|
|
dev->txpos = 0;
|
|
}
|
|
break;
|
|
}
|
|
dev->txstate = STATE_TRAILER;
|
|
dev->txpos = 0;
|
|
fallthrough;
|
|
|
|
case STATE_TRAILER:
|
|
buf[0] = dev->txfcs >> 8;
|
|
buf[1] = dev->txfcs & 0xff;
|
|
buf[2] = BYTE_FRAME;
|
|
txlen = write_chunk(dev, buf + dev->txpos, 3 - dev->txpos);
|
|
if (txlen <= 0) {
|
|
dev->txstate = STATE_ERR;
|
|
} else {
|
|
dev->txpos += txlen;
|
|
if (dev->txpos == 3) {
|
|
dev->txstate = STATE_DONE;
|
|
dev->txpos = 0;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
netdev_err_once(dev->netdev, "invalid tx state %d\n",
|
|
dev->txstate);
|
|
}
|
|
|
|
if (dev->txstate == STATE_DONE) {
|
|
dev->netdev->stats.tx_packets++;
|
|
dev->netdev->stats.tx_bytes += dev->txlen;
|
|
dev->txlen = 0;
|
|
dev->txpos = 0;
|
|
clear_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags);
|
|
dev->txstate = STATE_IDLE;
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
|
|
netif_wake_queue(dev->netdev);
|
|
} else {
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
}
|
|
}
|
|
|
|
static netdev_tx_t mctp_serial_tx(struct sk_buff *skb, struct net_device *ndev)
|
|
{
|
|
struct mctp_serial *dev = netdev_priv(ndev);
|
|
unsigned long flags;
|
|
|
|
WARN_ON(dev->txstate != STATE_IDLE);
|
|
|
|
if (skb->len > MCTP_SERIAL_MTU) {
|
|
dev->netdev->stats.tx_dropped++;
|
|
goto out;
|
|
}
|
|
|
|
spin_lock_irqsave(&dev->lock, flags);
|
|
netif_stop_queue(dev->netdev);
|
|
skb_copy_bits(skb, 0, dev->txbuf, skb->len);
|
|
dev->txpos = 0;
|
|
dev->txlen = skb->len;
|
|
dev->txstate = STATE_START;
|
|
spin_unlock_irqrestore(&dev->lock, flags);
|
|
|
|
set_bit(TTY_DO_WRITE_WAKEUP, &dev->tty->flags);
|
|
schedule_work(&dev->tx_work);
|
|
|
|
out:
|
|
kfree_skb(skb);
|
|
return NETDEV_TX_OK;
|
|
}
|
|
|
|
static void mctp_serial_tty_write_wakeup(struct tty_struct *tty)
|
|
{
|
|
struct mctp_serial *dev = tty->disc_data;
|
|
|
|
schedule_work(&dev->tx_work);
|
|
}
|
|
|
|
static void mctp_serial_rx(struct mctp_serial *dev)
|
|
{
|
|
struct mctp_skb_cb *cb;
|
|
struct sk_buff *skb;
|
|
|
|
if (dev->rxfcs != dev->rxfcs_rcvd) {
|
|
dev->netdev->stats.rx_dropped++;
|
|
dev->netdev->stats.rx_crc_errors++;
|
|
return;
|
|
}
|
|
|
|
skb = netdev_alloc_skb(dev->netdev, dev->rxlen);
|
|
if (!skb) {
|
|
dev->netdev->stats.rx_dropped++;
|
|
return;
|
|
}
|
|
|
|
skb->protocol = htons(ETH_P_MCTP);
|
|
skb_put_data(skb, dev->rxbuf, dev->rxlen);
|
|
skb_reset_network_header(skb);
|
|
|
|
cb = __mctp_cb(skb);
|
|
cb->halen = 0;
|
|
|
|
netif_rx(skb);
|
|
dev->netdev->stats.rx_packets++;
|
|
dev->netdev->stats.rx_bytes += dev->rxlen;
|
|
}
|
|
|
|
static void mctp_serial_push_header(struct mctp_serial *dev, u8 c)
|
|
{
|
|
switch (dev->rxpos) {
|
|
case 0:
|
|
if (c == BYTE_FRAME)
|
|
dev->rxpos++;
|
|
else
|
|
dev->rxstate = STATE_ERR;
|
|
break;
|
|
case 1:
|
|
if (c == MCTP_SERIAL_VERSION) {
|
|
dev->rxpos++;
|
|
dev->rxfcs = crc_ccitt_byte(FCS_INIT, c);
|
|
} else {
|
|
dev->rxstate = STATE_ERR;
|
|
}
|
|
break;
|
|
case 2:
|
|
if (c > MCTP_SERIAL_FRAME_MTU) {
|
|
dev->rxstate = STATE_ERR;
|
|
} else {
|
|
dev->rxlen = c;
|
|
dev->rxpos = 0;
|
|
dev->rxstate = STATE_DATA;
|
|
dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void mctp_serial_push_trailer(struct mctp_serial *dev, u8 c)
|
|
{
|
|
switch (dev->rxpos) {
|
|
case 0:
|
|
dev->rxfcs_rcvd = c << 8;
|
|
dev->rxpos++;
|
|
break;
|
|
case 1:
|
|
dev->rxfcs_rcvd |= c;
|
|
dev->rxpos++;
|
|
break;
|
|
case 2:
|
|
if (c != BYTE_FRAME) {
|
|
dev->rxstate = STATE_ERR;
|
|
} else {
|
|
mctp_serial_rx(dev);
|
|
dev->rxlen = 0;
|
|
dev->rxpos = 0;
|
|
dev->rxstate = STATE_IDLE;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void mctp_serial_push(struct mctp_serial *dev, u8 c)
|
|
{
|
|
switch (dev->rxstate) {
|
|
case STATE_IDLE:
|
|
dev->rxstate = STATE_HEADER;
|
|
fallthrough;
|
|
case STATE_HEADER:
|
|
mctp_serial_push_header(dev, c);
|
|
break;
|
|
|
|
case STATE_ESCAPE:
|
|
c |= 0x20;
|
|
fallthrough;
|
|
case STATE_DATA:
|
|
if (dev->rxstate != STATE_ESCAPE && c == BYTE_ESC) {
|
|
dev->rxstate = STATE_ESCAPE;
|
|
} else {
|
|
dev->rxfcs = crc_ccitt_byte(dev->rxfcs, c);
|
|
dev->rxbuf[dev->rxpos] = c;
|
|
dev->rxpos++;
|
|
dev->rxstate = STATE_DATA;
|
|
if (dev->rxpos == dev->rxlen) {
|
|
dev->rxpos = 0;
|
|
dev->rxstate = STATE_TRAILER;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case STATE_TRAILER:
|
|
mctp_serial_push_trailer(dev, c);
|
|
break;
|
|
|
|
case STATE_ERR:
|
|
if (c == BYTE_FRAME)
|
|
dev->rxstate = STATE_IDLE;
|
|
break;
|
|
|
|
default:
|
|
netdev_err_once(dev->netdev, "invalid rx state %d\n",
|
|
dev->rxstate);
|
|
}
|
|
}
|
|
|
|
static void mctp_serial_tty_receive_buf(struct tty_struct *tty, const u8 *c,
|
|
const u8 *f, size_t len)
|
|
{
|
|
struct mctp_serial *dev = tty->disc_data;
|
|
size_t i;
|
|
|
|
if (!netif_running(dev->netdev))
|
|
return;
|
|
|
|
/* we don't (currently) use the flag bytes, just data. */
|
|
for (i = 0; i < len; i++)
|
|
mctp_serial_push(dev, c[i]);
|
|
}
|
|
|
|
static void mctp_serial_uninit(struct net_device *ndev)
|
|
{
|
|
struct mctp_serial *dev = netdev_priv(ndev);
|
|
|
|
cancel_work_sync(&dev->tx_work);
|
|
}
|
|
|
|
static const struct net_device_ops mctp_serial_netdev_ops = {
|
|
.ndo_start_xmit = mctp_serial_tx,
|
|
.ndo_uninit = mctp_serial_uninit,
|
|
};
|
|
|
|
static void mctp_serial_setup(struct net_device *ndev)
|
|
{
|
|
ndev->type = ARPHRD_MCTP;
|
|
|
|
/* we limit at the fixed MTU, which is also the MCTP-standard
|
|
* baseline MTU, so is also our minimum
|
|
*/
|
|
ndev->mtu = MCTP_SERIAL_MTU;
|
|
ndev->max_mtu = MCTP_SERIAL_MTU;
|
|
ndev->min_mtu = MCTP_SERIAL_MTU;
|
|
|
|
ndev->hard_header_len = 0;
|
|
ndev->addr_len = 0;
|
|
ndev->tx_queue_len = DEFAULT_TX_QUEUE_LEN;
|
|
ndev->flags = IFF_NOARP;
|
|
ndev->netdev_ops = &mctp_serial_netdev_ops;
|
|
ndev->needs_free_netdev = true;
|
|
}
|
|
|
|
static int mctp_serial_open(struct tty_struct *tty)
|
|
{
|
|
struct mctp_serial *dev;
|
|
struct net_device *ndev;
|
|
char name[32];
|
|
int idx, rc;
|
|
|
|
if (!capable(CAP_NET_ADMIN))
|
|
return -EPERM;
|
|
|
|
if (!tty->ops->write)
|
|
return -EOPNOTSUPP;
|
|
|
|
idx = ida_alloc(&mctp_serial_ida, GFP_KERNEL);
|
|
if (idx < 0)
|
|
return idx;
|
|
|
|
snprintf(name, sizeof(name), "mctpserial%d", idx);
|
|
ndev = alloc_netdev(sizeof(*dev), name, NET_NAME_ENUM,
|
|
mctp_serial_setup);
|
|
if (!ndev) {
|
|
rc = -ENOMEM;
|
|
goto free_ida;
|
|
}
|
|
|
|
dev = netdev_priv(ndev);
|
|
dev->idx = idx;
|
|
dev->tty = tty;
|
|
dev->netdev = ndev;
|
|
dev->txstate = STATE_IDLE;
|
|
dev->rxstate = STATE_IDLE;
|
|
spin_lock_init(&dev->lock);
|
|
INIT_WORK(&dev->tx_work, mctp_serial_tx_work);
|
|
|
|
rc = mctp_register_netdev(ndev, NULL, MCTP_PHYS_BINDING_SERIAL);
|
|
if (rc)
|
|
goto free_netdev;
|
|
|
|
tty->receive_room = 64 * 1024;
|
|
tty->disc_data = dev;
|
|
|
|
return 0;
|
|
|
|
free_netdev:
|
|
free_netdev(ndev);
|
|
|
|
free_ida:
|
|
ida_free(&mctp_serial_ida, idx);
|
|
return rc;
|
|
}
|
|
|
|
static void mctp_serial_close(struct tty_struct *tty)
|
|
{
|
|
struct mctp_serial *dev = tty->disc_data;
|
|
int idx = dev->idx;
|
|
|
|
mctp_unregister_netdev(dev->netdev);
|
|
ida_free(&mctp_serial_ida, idx);
|
|
}
|
|
|
|
static struct tty_ldisc_ops mctp_ldisc = {
|
|
.owner = THIS_MODULE,
|
|
.num = N_MCTP,
|
|
.name = "mctp",
|
|
.open = mctp_serial_open,
|
|
.close = mctp_serial_close,
|
|
.receive_buf = mctp_serial_tty_receive_buf,
|
|
.write_wakeup = mctp_serial_tty_write_wakeup,
|
|
};
|
|
|
|
static int __init mctp_serial_init(void)
|
|
{
|
|
return tty_register_ldisc(&mctp_ldisc);
|
|
}
|
|
|
|
static void __exit mctp_serial_exit(void)
|
|
{
|
|
tty_unregister_ldisc(&mctp_ldisc);
|
|
}
|
|
|
|
module_init(mctp_serial_init);
|
|
module_exit(mctp_serial_exit);
|
|
|
|
MODULE_LICENSE("GPL v2");
|
|
MODULE_AUTHOR("Jeremy Kerr <jk@codeconstruct.com.au>");
|
|
MODULE_DESCRIPTION("MCTP Serial transport");
|
|
|
|
#if IS_ENABLED(CONFIG_MCTP_SERIAL_TEST)
|
|
#include <kunit/test.h>
|
|
|
|
#define MAX_CHUNKS 6
|
|
struct test_chunk_tx {
|
|
u8 input_len;
|
|
u8 input[MCTP_SERIAL_MTU];
|
|
u8 chunks[MAX_CHUNKS];
|
|
};
|
|
|
|
static void test_next_chunk_len(struct kunit *test)
|
|
{
|
|
struct mctp_serial devx;
|
|
struct mctp_serial *dev = &devx;
|
|
int next;
|
|
|
|
const struct test_chunk_tx *params = test->param_value;
|
|
|
|
memset(dev, 0x0, sizeof(*dev));
|
|
memcpy(dev->txbuf, params->input, params->input_len);
|
|
dev->txlen = params->input_len;
|
|
|
|
for (size_t i = 0; i < MAX_CHUNKS; i++) {
|
|
next = next_chunk_len(dev);
|
|
dev->txpos += next;
|
|
KUNIT_EXPECT_EQ(test, next, params->chunks[i]);
|
|
|
|
if (next == 0) {
|
|
KUNIT_EXPECT_EQ(test, dev->txpos, dev->txlen);
|
|
return;
|
|
}
|
|
}
|
|
|
|
KUNIT_FAIL_AND_ABORT(test, "Ran out of chunks");
|
|
}
|
|
|
|
static struct test_chunk_tx chunk_tx_tests[] = {
|
|
{
|
|
.input_len = 5,
|
|
.input = { 0x00, 0x11, 0x22, 0x7e, 0x80 },
|
|
.chunks = { 3, 1, 1, 0},
|
|
},
|
|
{
|
|
.input_len = 5,
|
|
.input = { 0x00, 0x11, 0x22, 0x7e, 0x7d },
|
|
.chunks = { 3, 1, 1, 0},
|
|
},
|
|
{
|
|
.input_len = 3,
|
|
.input = { 0x7e, 0x11, 0x22, },
|
|
.chunks = { 1, 2, 0},
|
|
},
|
|
{
|
|
.input_len = 3,
|
|
.input = { 0x7e, 0x7e, 0x7d, },
|
|
.chunks = { 1, 1, 1, 0},
|
|
},
|
|
{
|
|
.input_len = 4,
|
|
.input = { 0x7e, 0x7e, 0x00, 0x7d, },
|
|
.chunks = { 1, 1, 1, 1, 0},
|
|
},
|
|
{
|
|
.input_len = 6,
|
|
.input = { 0x7e, 0x7e, 0x00, 0x7d, 0x10, 0x10},
|
|
.chunks = { 1, 1, 1, 1, 2, 0},
|
|
},
|
|
{
|
|
.input_len = 1,
|
|
.input = { 0x7e },
|
|
.chunks = { 1, 0 },
|
|
},
|
|
{
|
|
.input_len = 1,
|
|
.input = { 0x80 },
|
|
.chunks = { 1, 0 },
|
|
},
|
|
{
|
|
.input_len = 3,
|
|
.input = { 0x80, 0x80, 0x00 },
|
|
.chunks = { 3, 0 },
|
|
},
|
|
{
|
|
.input_len = 7,
|
|
.input = { 0x01, 0x00, 0x08, 0xc8, 0x00, 0x80, 0x02 },
|
|
.chunks = { 7, 0 },
|
|
},
|
|
{
|
|
.input_len = 7,
|
|
.input = { 0x01, 0x00, 0x08, 0xc8, 0x7e, 0x80, 0x02 },
|
|
.chunks = { 4, 1, 2, 0 },
|
|
},
|
|
};
|
|
|
|
KUNIT_ARRAY_PARAM(chunk_tx, chunk_tx_tests, NULL);
|
|
|
|
static struct kunit_case mctp_serial_test_cases[] = {
|
|
KUNIT_CASE_PARAM(test_next_chunk_len, chunk_tx_gen_params),
|
|
};
|
|
|
|
static struct kunit_suite mctp_serial_test_suite = {
|
|
.name = "mctp_serial",
|
|
.test_cases = mctp_serial_test_cases,
|
|
};
|
|
|
|
kunit_test_suite(mctp_serial_test_suite);
|
|
|
|
#endif /* CONFIG_MCTP_SERIAL_TEST */
|