mirror of
https://github.com/torvalds/linux.git
synced 2026-10-17 06:20:02 +02:00
A range of managed Realtek-based PoE switches use a small microcontroller on the PCB to front the actual PSE silicon. The host CPU talks to that MCU over I2C/SMBus or UART using a fixed 12-byte request/response protocol with a trailing checksum; the PSE chips are managed by the MCU and are not accessed directly. Two generations of the protocol exist - both Realtek's - diverging in opcode numbering and a few response layouts; the driver handles this with a per-dialect opcode table and parser hooks for the responses that differ, selected by the compatible. The specific PSE chip behind the MCU is detected at runtime and only influences per-chip constants (power scaling and the per-port cap). This core module implements the protocol, message framing, the dialect machinery and the pse_controller_ops glue, and exports a registration helper for transport modules. The I2C and UART transports that drive it follow in the next patches; the core (PSE_REALTEK_MCU) is selected automatically by those transports and is not user-selectable on its own. The realtek-pse-mcu-* files and PSE_REALTEK_MCU* symbols match the realtek,pse-mcu-* compatibles (see the binding for the naming rationale). The two protocol generations - gen1 on older Broadcom-PSE boards, gen2 on Realtek's own PSE silicon - are both Realtek's, handled by the same shared core, each selecting its dialect via the compatible. Power budgeting is left to the MCU firmware; the driver advertises PSE_BUDGET_EVAL_STRAT_DYNAMIC accordingly. Signed-off-by: Jonas Jelonek <jelonek.jonas@gmail.com> Link: https://patch.msgid.link/20260813222036.873930-3-jelonek.jonas@gmail.com Signed-off-by: Paolo Abeni <pabeni@redhat.com>
999 lines
29 KiB
C
999 lines
29 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Driver for the microcontroller (MCU) fronting PSE silicon on various
|
|
* Realtek-based managed switches. The MCU speaks a 12-byte fixed-frame
|
|
* management protocol; this driver covers two generations of the
|
|
* protocol via a per-dialect opcode table and response parsers.
|
|
*
|
|
* Many PoE switch designs put a dedicated microcontroller in front of the
|
|
* actual PSE silicon: the host CPU talks to the MCU over I2C/SMBus or
|
|
* UART, and the MCU in turn manages the PSE chips on the board. The MCU
|
|
* speaks a small message-based protocol. The PSE chips themselves are not
|
|
* accessed directly; everything goes through MCU commands.
|
|
*
|
|
* This driver targets that architecture for the Realtek-family protocol.
|
|
* Two generations are supported: Gen1 being used on older switches where
|
|
* the MCU fronts and manages Broadcom PSE silicon; Gen2 being used with
|
|
* Realtek PSE silicon. The two share frame format and a sum-mod-256
|
|
* checksum but diverge on opcode numbers and on a few response layouts;
|
|
* this is handled by the per-dialect opcode table and parser hooks.
|
|
*
|
|
* Out of scope: PSE chips that are interfaced directly from the host
|
|
* without a management MCU, MCU designs that speak an unrelated protocol
|
|
* family, and "dumb PSE" modes where no host control is wired up at all.
|
|
*
|
|
* This core module implements the protocol, decoding/encoding of MCU
|
|
* responses, and the pse_controller_ops integration. Transport modules
|
|
* (realtek-pse-mcu-i2c, realtek-pse-mcu-uart) provide the send/recv
|
|
* callbacks.
|
|
*/
|
|
|
|
#include <linux/bitfield.h>
|
|
#include <linux/cleanup.h>
|
|
#include <linux/container_of.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/gpio/consumer.h>
|
|
#include <linux/jiffies.h>
|
|
#include <linux/minmax.h>
|
|
#include <linux/module.h>
|
|
#include <linux/property.h>
|
|
#include <linux/pse-pd/pse.h>
|
|
#include <linux/unaligned.h>
|
|
|
|
#include "realtek-pse-mcu.h"
|
|
|
|
#define RTPSE_MCU_DEVICE_ID_RTL8238B 0x0138
|
|
#define RTPSE_MCU_DEVICE_ID_RTL8239 0x0039
|
|
#define RTPSE_MCU_DEVICE_ID_RTL8239C 0x0139
|
|
#define RTPSE_MCU_DEVICE_ID_BCM59111 0xe111
|
|
#define RTPSE_MCU_DEVICE_ID_BCM59121 0xe121
|
|
|
|
#define RTPSE_MCU_PORT_STS_DISABLED 0x00
|
|
#define RTPSE_MCU_PORT_STS_SEARCHING 0x01
|
|
#define RTPSE_MCU_PORT_STS_DELIVERING 0x02
|
|
#define RTPSE_MCU_PORT_STS_TEST 0x03 /* Gen1-only; reserved on Gen2 */
|
|
#define RTPSE_MCU_PORT_STS_FAULT 0x04
|
|
#define RTPSE_MCU_PORT_STS_OTHER_FAULT 0x05 /* Gen1-only; reserved on Gen2 */
|
|
#define RTPSE_MCU_PORT_STS_REQUESTING 0x06
|
|
|
|
/* RTPSE_MCU_PORT_SET_POWER_LIMIT_TYPE values */
|
|
#define RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER 0x02
|
|
|
|
#define RTPSE_MCU_MAX_PORTS 48
|
|
#define RTPSE_MCU_PORT_MAX_PRIORITY 3
|
|
|
|
/* Bounded resends when the MCU replies NOT_READY (busy). */
|
|
#define RTPSE_MCU_NOT_READY_RETRIES 3
|
|
|
|
/* Nominal PSE rail; 802.3at/bt operating range. */
|
|
#define RTPSE_MCU_PSE_VOLTAGE_UV 54000000
|
|
|
|
enum rtpse_mcu_cmd {
|
|
RTPSE_MCU_CMD_SET_GLOBAL_STATE,
|
|
RTPSE_MCU_CMD_GET_SYSTEM_INFO,
|
|
RTPSE_MCU_CMD_GET_EXT_CONFIG,
|
|
|
|
RTPSE_MCU_CMD_PORT_ENABLE,
|
|
RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE,
|
|
RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
|
|
RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
|
|
RTPSE_MCU_CMD_PORT_SET_PRIORITY,
|
|
RTPSE_MCU_CMD_PORT_GET_STATUS,
|
|
RTPSE_MCU_CMD_PORT_GET_POWER_STATS,
|
|
RTPSE_MCU_CMD_PORT_GET_CONFIG,
|
|
RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG,
|
|
|
|
RTPSE_MCU_NUM_CMDS,
|
|
};
|
|
|
|
struct rtpse_mcu_opcode {
|
|
u8 op;
|
|
bool valid;
|
|
};
|
|
|
|
/* Shorthand for the designated-initializer entries in dialect opcode tables. */
|
|
#define RTPSE_MCU_OP(opc) { .op = (opc), .valid = true }
|
|
|
|
/* Parsed MCU response structures (decoded from rtpse_mcu_msg replies) */
|
|
|
|
struct rtpse_mcu_info {
|
|
u8 max_ports;
|
|
bool system_enable;
|
|
u16 device_id;
|
|
u8 mcu_type;
|
|
};
|
|
|
|
struct rtpse_mcu_ext_config {
|
|
u8 num_of_pses;
|
|
};
|
|
|
|
struct rtpse_mcu_port_status {
|
|
u8 sts1;
|
|
u8 sts2;
|
|
u8 sts3;
|
|
};
|
|
|
|
struct rtpse_mcu_port_measurement {
|
|
u16 voltage_raw; /* 64.45mV/LSB */
|
|
u16 current_raw; /* 1mA/LSB */
|
|
u16 temperature_raw; /* T(mC) = 1250 * (220 - raw) */
|
|
u16 power_raw; /* 100mW/LSB */
|
|
};
|
|
|
|
struct rtpse_mcu_port_config {
|
|
bool enable;
|
|
};
|
|
|
|
struct rtpse_mcu_port_ext_config {
|
|
u8 max_power;
|
|
u8 priority;
|
|
};
|
|
|
|
struct rtpse_mcu_dialect {
|
|
struct rtpse_mcu_opcode opcode[RTPSE_MCU_NUM_CMDS];
|
|
|
|
/*
|
|
* Response parsers for the fields that differ between dialects; each
|
|
* dialect supplies its own. Other responses share one layout and are
|
|
* decoded directly - a dialect that diverges there must add a hook,
|
|
* as a mismatched layout cannot be detected (the checksum still passes).
|
|
*/
|
|
void (*parse_system_info)(const u8 *payload, struct rtpse_mcu_info *info);
|
|
int (*parse_port_class)(const struct rtpse_mcu_port_status *status);
|
|
const char *(*mcu_type_str)(unsigned int mcu_type);
|
|
};
|
|
|
|
struct rtpse_mcu_chip_info {
|
|
const char *name;
|
|
u32 max_mW_per_port;
|
|
enum rtpse_mcu_cmd pw_set_cmd; /* command used by set_pw_limit */
|
|
u32 pw_set_lsb_mW; /* LSB of pw_set_cmd value, in mW */
|
|
u32 pw_read_lsb_mW; /* LSB of ext_config.max_power read-back, in mW */
|
|
};
|
|
|
|
static const struct rtpse_mcu_chip_info rtl8238b_info = {
|
|
.max_mW_per_port = 30000,
|
|
.name = "RTL8238B",
|
|
.pw_read_lsb_mW = 200,
|
|
.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
|
|
.pw_set_lsb_mW = 200,
|
|
};
|
|
|
|
static const struct rtpse_mcu_chip_info rtl8239_info = {
|
|
.max_mW_per_port = 90000,
|
|
.name = "RTL8239",
|
|
.pw_read_lsb_mW = 400,
|
|
.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
|
|
.pw_set_lsb_mW = 400,
|
|
};
|
|
|
|
static const struct rtpse_mcu_chip_info rtl8239c_info = {
|
|
.max_mW_per_port = 90000,
|
|
.name = "RTL8239C",
|
|
.pw_read_lsb_mW = 400,
|
|
.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT,
|
|
.pw_set_lsb_mW = 400,
|
|
};
|
|
|
|
static const struct rtpse_mcu_chip_info bcm59111_info = {
|
|
.max_mW_per_port = 30000,
|
|
.name = "BCM59111",
|
|
.pw_read_lsb_mW = 200,
|
|
.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
|
|
.pw_set_lsb_mW = 200,
|
|
};
|
|
|
|
static const struct rtpse_mcu_chip_info bcm59121_info = {
|
|
/*
|
|
* BCM59121 is a 60W Type-3 part, but known boards run it at 802.3at
|
|
* and the Gen1 dialect has only the 8-bit/0.2W set command (<=51W);
|
|
* cap at the 30W the hardware actually offers.
|
|
*/
|
|
.max_mW_per_port = 30000,
|
|
.name = "BCM59121",
|
|
.pw_read_lsb_mW = 200,
|
|
.pw_set_cmd = RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT,
|
|
.pw_set_lsb_mW = 200,
|
|
};
|
|
|
|
/* Helpers and basic functions */
|
|
|
|
static struct rtpse_mcu_ctrl *to_rtpse_mcu_ctrl(struct pse_controller_dev *pcdev)
|
|
{
|
|
return container_of(pcdev, struct rtpse_mcu_ctrl, pcdev);
|
|
}
|
|
|
|
static void rtpse_mcu_msg_init(struct rtpse_mcu_msg *msg, u8 opcode)
|
|
{
|
|
memset(msg, 0xff, sizeof(*msg));
|
|
msg->opcode = opcode;
|
|
}
|
|
|
|
static u8 rtpse_mcu_checksum(const u8 *buf, size_t len)
|
|
{
|
|
u8 sum = 0;
|
|
|
|
while (len--)
|
|
sum += *buf++;
|
|
return sum;
|
|
}
|
|
|
|
static int rtpse_mcu_do_xfer(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_msg *req,
|
|
struct rtpse_mcu_msg *resp)
|
|
{
|
|
unsigned int tries;
|
|
int ret;
|
|
|
|
for (tries = 0; ; tries++) {
|
|
scoped_guard(mutex, &pse->mutex) {
|
|
/* Rolling seq_num (skip 0) so a stale/all-zero reply can't match. */
|
|
if (++pse->seq == 0)
|
|
pse->seq = 1;
|
|
req->seq_num = pse->seq;
|
|
req->checksum = rtpse_mcu_checksum((u8 *)req, RTPSE_MCU_MSG_SIZE - 1);
|
|
|
|
ret = pse->transport->send(pse, req);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Pace the base reply delay; the transport waits its own way. */
|
|
msleep(RTPSE_MCU_RESPONSE_MS);
|
|
|
|
memset(resp, 0, sizeof(*resp));
|
|
ret = pse->transport->recv(pse, req, resp);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
|
|
/* NOT_READY: MCU busy, wants the command resent; bounded retry. */
|
|
if (resp->opcode != RTPSE_MCU_OPCODE_NOT_READY ||
|
|
tries >= RTPSE_MCU_NOT_READY_RETRIES)
|
|
break;
|
|
msleep(RTPSE_MCU_RESPONSE_MS);
|
|
}
|
|
|
|
/* Explicit MCU error opcodes (Gen1); map to a meaningful errno. */
|
|
switch (resp->opcode) {
|
|
case RTPSE_MCU_OPCODE_INCOMPLETE:
|
|
return -EBADE;
|
|
case RTPSE_MCU_OPCODE_BAD_CSUM:
|
|
return -EBADMSG;
|
|
case RTPSE_MCU_OPCODE_NOT_READY:
|
|
return -EAGAIN;
|
|
}
|
|
|
|
if (resp->opcode != req->opcode ||
|
|
resp->seq_num != req->seq_num ||
|
|
resp->checksum != rtpse_mcu_checksum((u8 *)resp, RTPSE_MCU_MSG_SIZE - 1))
|
|
return -EBADMSG;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_query(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode,
|
|
struct rtpse_mcu_msg *resp)
|
|
{
|
|
struct rtpse_mcu_msg req;
|
|
int ret;
|
|
|
|
rtpse_mcu_msg_init(&req, opcode);
|
|
req.payload[0] = port;
|
|
|
|
ret = rtpse_mcu_do_xfer(pse, &req, resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (resp->payload[0] != port)
|
|
return -EIO;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_cmd(struct rtpse_mcu_ctrl *pse, unsigned int port, u8 opcode, u8 arg)
|
|
{
|
|
struct rtpse_mcu_msg req, resp;
|
|
int ret;
|
|
|
|
rtpse_mcu_msg_init(&req, opcode);
|
|
req.payload[0] = port;
|
|
req.payload[1] = arg;
|
|
|
|
ret = rtpse_mcu_do_xfer(pse, &req, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (resp.payload[0] != port || resp.payload[1] != 0)
|
|
return -EIO;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* Global operations */
|
|
|
|
static int rtpse_mcu_get_info(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
|
|
{
|
|
struct rtpse_mcu_msg req, resp;
|
|
const struct rtpse_mcu_opcode *opc;
|
|
int ret;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_SYSTEM_INFO];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
rtpse_mcu_msg_init(&req, opc->op);
|
|
ret = rtpse_mcu_do_xfer(pse, &req, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
pse->dialect->parse_system_info(resp.payload, info);
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_get_ext_config(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_ext_config *config)
|
|
{
|
|
struct rtpse_mcu_msg req, resp;
|
|
const struct rtpse_mcu_opcode *opc;
|
|
int ret;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_GET_EXT_CONFIG];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
rtpse_mcu_msg_init(&req, opc->op);
|
|
ret = rtpse_mcu_do_xfer(pse, &req, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
config->num_of_pses = resp.payload[6];
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_set_global_state(struct rtpse_mcu_ctrl *pse, bool enable)
|
|
{
|
|
struct rtpse_mcu_msg req, resp;
|
|
const struct rtpse_mcu_opcode *opc;
|
|
int ret;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_SET_GLOBAL_STATE];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
rtpse_mcu_msg_init(&req, opc->op);
|
|
req.payload[0] = enable ? 0x1 : 0x0;
|
|
|
|
ret = rtpse_mcu_do_xfer(pse, &req, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return (resp.payload[0] == 0x0) ? 0 : -EIO;
|
|
}
|
|
|
|
/* Port operations */
|
|
|
|
static int rtpse_mcu_port_get_status(struct rtpse_mcu_ctrl *pse, unsigned int port,
|
|
struct rtpse_mcu_port_status *status)
|
|
{
|
|
const struct rtpse_mcu_opcode *opc;
|
|
struct rtpse_mcu_msg resp;
|
|
int ret;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_STATUS];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
status->sts1 = resp.payload[1];
|
|
status->sts2 = resp.payload[2];
|
|
status->sts3 = resp.payload[3];
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_measurement(struct rtpse_mcu_ctrl *pse, unsigned int port,
|
|
struct rtpse_mcu_port_measurement *measurement)
|
|
{
|
|
const struct rtpse_mcu_opcode *opc;
|
|
struct rtpse_mcu_msg resp;
|
|
int ret;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_POWER_STATS];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
measurement->voltage_raw = get_unaligned_be16(&resp.payload[1]);
|
|
measurement->current_raw = get_unaligned_be16(&resp.payload[3]);
|
|
measurement->temperature_raw = get_unaligned_be16(&resp.payload[5]);
|
|
measurement->power_raw = get_unaligned_be16(&resp.payload[7]);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
|
|
struct rtpse_mcu_port_config *config)
|
|
{
|
|
const struct rtpse_mcu_opcode *opc;
|
|
struct rtpse_mcu_msg resp;
|
|
int ret;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_CONFIG];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
config->enable = (resp.payload[1] == 1);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_ext_config(struct rtpse_mcu_ctrl *pse, unsigned int port,
|
|
struct rtpse_mcu_port_ext_config *config)
|
|
{
|
|
const struct rtpse_mcu_opcode *opc;
|
|
struct rtpse_mcu_msg resp;
|
|
int ret;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
ret = rtpse_mcu_port_query(pse, port, opc->op, &resp);
|
|
if (ret)
|
|
return ret;
|
|
|
|
config->max_power = resp.payload[3];
|
|
config->priority = resp.payload[4];
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_set_state(struct rtpse_mcu_ctrl *pse, unsigned int port, bool enable)
|
|
{
|
|
const struct rtpse_mcu_opcode *opc;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_ENABLE];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
return rtpse_mcu_port_cmd(pse, port, opc->op, enable ? 0x1 : 0x0);
|
|
}
|
|
|
|
/* PSE controller ops */
|
|
|
|
static int rtpse_mcu_port_get_admin_state(struct pse_controller_dev *pcdev, int id,
|
|
struct pse_admin_state *admin_state)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct rtpse_mcu_port_config config;
|
|
int ret;
|
|
|
|
ret = rtpse_mcu_port_get_config(pse, id, &config);
|
|
if (ret)
|
|
return ret;
|
|
|
|
admin_state->c33_admin_state = config.enable ? ETHTOOL_C33_PSE_ADMIN_STATE_ENABLED :
|
|
ETHTOOL_C33_PSE_ADMIN_STATE_DISABLED;
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_pw_status(struct pse_controller_dev *pcdev, int id,
|
|
struct pse_pw_status *pw_status)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct rtpse_mcu_port_status status;
|
|
int ret;
|
|
|
|
ret = rtpse_mcu_port_get_status(pse, id, &status);
|
|
if (ret)
|
|
return ret;
|
|
|
|
switch (status.sts1) {
|
|
case RTPSE_MCU_PORT_STS_DISABLED:
|
|
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DISABLED;
|
|
break;
|
|
case RTPSE_MCU_PORT_STS_SEARCHING:
|
|
case RTPSE_MCU_PORT_STS_REQUESTING:
|
|
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_SEARCHING;
|
|
break;
|
|
case RTPSE_MCU_PORT_STS_DELIVERING:
|
|
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_DELIVERING;
|
|
break;
|
|
case RTPSE_MCU_PORT_STS_TEST:
|
|
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_TEST;
|
|
break;
|
|
case RTPSE_MCU_PORT_STS_FAULT:
|
|
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_FAULT;
|
|
break;
|
|
case RTPSE_MCU_PORT_STS_OTHER_FAULT:
|
|
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_OTHERFAULT;
|
|
break;
|
|
default:
|
|
pw_status->c33_pw_status = ETHTOOL_C33_PSE_PW_D_STATUS_UNKNOWN;
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_pw_class(struct pse_controller_dev *pcdev, int id)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct rtpse_mcu_port_status status;
|
|
int ret;
|
|
|
|
ret = rtpse_mcu_port_get_status(pse, id, &status);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/*
|
|
* As per datasheet, the classification result is only valid when in
|
|
* one of those operational modes, otherwise not.
|
|
*/
|
|
switch (status.sts1) {
|
|
case RTPSE_MCU_PORT_STS_DISABLED:
|
|
case RTPSE_MCU_PORT_STS_SEARCHING:
|
|
case RTPSE_MCU_PORT_STS_DELIVERING:
|
|
case RTPSE_MCU_PORT_STS_REQUESTING:
|
|
return pse->dialect->parse_port_class(&status);
|
|
default:
|
|
/*
|
|
* No class to report, return 0 instead. This is indistinguishable
|
|
* from a real class-0 PD but userspace disambiguates via the
|
|
* power status.
|
|
*/
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_actual_pw(struct pse_controller_dev *pcdev, int id)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct rtpse_mcu_port_measurement measurement;
|
|
int ret;
|
|
|
|
ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* 100mW per LSB */
|
|
return measurement.power_raw * 100U;
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_voltage(struct pse_controller_dev *pcdev, int id)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct rtpse_mcu_port_measurement measurement;
|
|
int ret;
|
|
u32 uV;
|
|
|
|
ret = rtpse_mcu_port_get_measurement(pse, id, &measurement);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* 64.45mV per LSB */
|
|
uV = measurement.voltage_raw * 64450U;
|
|
|
|
/*
|
|
* Idle ports measure 0V, which the core rejects when turning a power
|
|
* limit into a current limit. Fall back to the nominal rail so a limit
|
|
* can be set before a PD is attached.
|
|
*/
|
|
if (!uV)
|
|
return RTPSE_MCU_PSE_VOLTAGE_UV;
|
|
|
|
return min_t(u32, uV, INT_MAX);
|
|
}
|
|
|
|
static int rtpse_mcu_port_enable(struct pse_controller_dev *pcdev, int id)
|
|
{
|
|
return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, true);
|
|
}
|
|
|
|
static int rtpse_mcu_port_disable(struct pse_controller_dev *pcdev, int id)
|
|
{
|
|
return rtpse_mcu_port_set_state(to_rtpse_mcu_ctrl(pcdev), id, false);
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_pw_limit(struct pse_controller_dev *pcdev, int id)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct rtpse_mcu_port_ext_config config;
|
|
int ret;
|
|
|
|
ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/*
|
|
* The MCU's raw max_power byte can scale above the chip's rated cap;
|
|
* clamp to the same bound set_pw_limit() and the advertised range use.
|
|
*/
|
|
return min_t(u32, config.max_power * pse->chip->pw_read_lsb_mW,
|
|
pse->chip->max_mW_per_port);
|
|
}
|
|
|
|
static int rtpse_mcu_port_set_pw_limit(struct pse_controller_dev *pcdev, int id, int max_mW)
|
|
{
|
|
const struct rtpse_mcu_opcode *type_opc, *val_opc;
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
const struct rtpse_mcu_chip_info *chip = pse->chip;
|
|
u8 prg_val;
|
|
int ret;
|
|
|
|
if (max_mW < 0 || max_mW > chip->max_mW_per_port)
|
|
return -ERANGE;
|
|
|
|
type_opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE];
|
|
val_opc = &pse->dialect->opcode[chip->pw_set_cmd];
|
|
/* pw_set_lsb_mW is the divisor below; reject a chip that lacks it. */
|
|
if (!type_opc->valid || !val_opc->valid || !chip->pw_set_lsb_mW)
|
|
return -EOPNOTSUPP;
|
|
|
|
/*
|
|
* Round up so a sub-LSB request maps to one LSB, not silently to 0;
|
|
* an explicit 0 still yields 0, and LSB-aligned maxima can't overshoot.
|
|
*/
|
|
prg_val = min_t(unsigned int, DIV_ROUND_UP(max_mW, chip->pw_set_lsb_mW), U8_MAX);
|
|
|
|
/*
|
|
* Program the value before switching to user-defined mode. The two
|
|
* commands aren't atomic, but this order never leaves a stale cap: a
|
|
* failure keeps the previous cap, or (already user mode) the requested.
|
|
*/
|
|
ret = rtpse_mcu_port_cmd(pse, id, val_opc->op, prg_val);
|
|
if (ret)
|
|
return ret;
|
|
|
|
return rtpse_mcu_port_cmd(pse, id, type_opc->op, RTPSE_MCU_PORT_PW_LIMIT_TYPE_USER);
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_pw_limit_ranges(struct pse_controller_dev *pcdev, int id,
|
|
struct pse_pw_limit_ranges *out)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct ethtool_c33_pse_pw_limit_range *range;
|
|
|
|
range = kzalloc_obj(*range);
|
|
if (!range)
|
|
return -ENOMEM;
|
|
|
|
range[0].min = 0;
|
|
range[0].max = pse->chip->max_mW_per_port;
|
|
|
|
out->c33_pw_limit_ranges = range;
|
|
return 1;
|
|
}
|
|
|
|
static int rtpse_mcu_port_get_prio(struct pse_controller_dev *pcdev, int id)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
struct rtpse_mcu_port_ext_config config;
|
|
int ret;
|
|
|
|
ret = rtpse_mcu_port_get_ext_config(pse, id, &config);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/* Clamp to the advertised max; set_prio() and pis_prio_max use the same bound. */
|
|
return min_t(u8, config.priority, RTPSE_MCU_PORT_MAX_PRIORITY);
|
|
}
|
|
|
|
static int rtpse_mcu_port_set_prio(struct pse_controller_dev *pcdev, int id, unsigned int prio)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = to_rtpse_mcu_ctrl(pcdev);
|
|
const struct rtpse_mcu_opcode *opc;
|
|
|
|
if (prio > RTPSE_MCU_PORT_MAX_PRIORITY)
|
|
return -ERANGE;
|
|
|
|
opc = &pse->dialect->opcode[RTPSE_MCU_CMD_PORT_SET_PRIORITY];
|
|
if (!opc->valid)
|
|
return -EOPNOTSUPP;
|
|
|
|
return rtpse_mcu_port_cmd(pse, id, opc->op, prio);
|
|
}
|
|
|
|
static const struct pse_controller_ops rtpse_mcu_ops = {
|
|
.pi_get_admin_state = rtpse_mcu_port_get_admin_state,
|
|
.pi_get_pw_status = rtpse_mcu_port_get_pw_status,
|
|
.pi_get_pw_class = rtpse_mcu_port_get_pw_class,
|
|
.pi_get_actual_pw = rtpse_mcu_port_get_actual_pw,
|
|
.pi_enable = rtpse_mcu_port_enable,
|
|
.pi_disable = rtpse_mcu_port_disable,
|
|
.pi_get_voltage = rtpse_mcu_port_get_voltage,
|
|
.pi_get_pw_limit = rtpse_mcu_port_get_pw_limit,
|
|
.pi_set_pw_limit = rtpse_mcu_port_set_pw_limit,
|
|
.pi_get_pw_limit_ranges = rtpse_mcu_port_get_pw_limit_ranges,
|
|
.pi_get_prio = rtpse_mcu_port_get_prio,
|
|
.pi_set_prio = rtpse_mcu_port_set_prio,
|
|
};
|
|
|
|
static int rtpse_mcu_discover(struct rtpse_mcu_ctrl *pse, struct rtpse_mcu_info *info)
|
|
{
|
|
struct rtpse_mcu_ext_config ext_config;
|
|
unsigned long deadline;
|
|
int ret;
|
|
|
|
/*
|
|
* A booting MCU may stay silent (-ETIMEDOUT), not ACK its address
|
|
* (-ENXIO / -EREMOTEIO), report not-ready (-EAGAIN), or emit a
|
|
* corrupt/partial frame (-EBADMSG / -EBADE). Retry those within a
|
|
* bounded window; other errors (e.g. -EOPNOTSUPP) are fatal and fail
|
|
* immediately.
|
|
*/
|
|
deadline = jiffies + msecs_to_jiffies(RTPSE_MCU_BOOT_TIMEOUT_MS);
|
|
do {
|
|
ret = rtpse_mcu_get_info(pse, info);
|
|
if (ret != -ETIMEDOUT && ret != -ENXIO && ret != -EREMOTEIO &&
|
|
ret != -EAGAIN && ret != -EBADMSG && ret != -EBADE)
|
|
break;
|
|
msleep(RTPSE_MCU_BOOT_RETRY_MS);
|
|
} while (time_before(jiffies, deadline));
|
|
if (ret)
|
|
return dev_err_probe(pse->dev, ret, "failed to read MCU info\n");
|
|
|
|
switch (info->device_id) {
|
|
case RTPSE_MCU_DEVICE_ID_RTL8238B:
|
|
pse->chip = &rtl8238b_info;
|
|
break;
|
|
case RTPSE_MCU_DEVICE_ID_RTL8239:
|
|
pse->chip = &rtl8239_info;
|
|
break;
|
|
case RTPSE_MCU_DEVICE_ID_RTL8239C:
|
|
pse->chip = &rtl8239c_info;
|
|
break;
|
|
case RTPSE_MCU_DEVICE_ID_BCM59111:
|
|
pse->chip = &bcm59111_info;
|
|
break;
|
|
case RTPSE_MCU_DEVICE_ID_BCM59121:
|
|
pse->chip = &bcm59121_info;
|
|
break;
|
|
default:
|
|
return dev_err_probe(pse->dev, -EINVAL, "unknown PSE id 0x%x\n",
|
|
info->device_id);
|
|
}
|
|
|
|
if (!info->max_ports || info->max_ports > RTPSE_MCU_MAX_PORTS)
|
|
return dev_err_probe(pse->dev, -EINVAL,
|
|
"MCU reports invalid port count %u\n", info->max_ports);
|
|
|
|
ret = rtpse_mcu_get_ext_config(pse, &ext_config);
|
|
if (ret)
|
|
return dev_err_probe(pse->dev, ret, "failed to read MCU ext config\n");
|
|
|
|
dev_info(pse->dev, "%s MCU, %s (id 0x%04x), %u ports across %u PSE chip(s)\n",
|
|
pse->dialect->mcu_type_str(info->mcu_type), pse->chip->name,
|
|
info->device_id, info->max_ports, ext_config.num_of_pses);
|
|
return 0;
|
|
}
|
|
|
|
static void rtpse_mcu_global_disable(void *data)
|
|
{
|
|
struct rtpse_mcu_ctrl *pse = data;
|
|
|
|
rtpse_mcu_set_global_state(pse, false);
|
|
}
|
|
|
|
int rtpse_mcu_register(struct rtpse_mcu_ctrl *pse)
|
|
{
|
|
const struct rtpse_mcu_match_data *match;
|
|
struct rtpse_mcu_info info;
|
|
struct gpio_desc *gpiod;
|
|
int ret;
|
|
|
|
BUILD_BUG_ON(sizeof(struct rtpse_mcu_msg) != RTPSE_MCU_MSG_SIZE);
|
|
|
|
ret = devm_mutex_init(pse->dev, &pse->mutex);
|
|
if (ret)
|
|
return ret;
|
|
|
|
match = device_get_match_data(pse->dev);
|
|
if (!match)
|
|
return dev_err_probe(pse->dev, -ENODEV, "missing match data\n");
|
|
pse->dialect = match->dialect;
|
|
|
|
/*
|
|
* Catch a dialect that forgot to set one of the required hooks at
|
|
* probe time, rather than NULL-deref'ing later from a fast path.
|
|
*/
|
|
if (!pse->dialect ||
|
|
!pse->dialect->parse_system_info ||
|
|
!pse->dialect->parse_port_class ||
|
|
!pse->dialect->mcu_type_str)
|
|
return dev_err_probe(pse->dev, -EINVAL,
|
|
"dialect for chip is incomplete\n");
|
|
|
|
/*
|
|
* Release the MCU from reset before the first transaction; the
|
|
* boot-retry loop in discover() waits for it to answer.
|
|
*/
|
|
gpiod = devm_gpiod_get_optional(pse->dev, "reset", GPIOD_OUT_LOW);
|
|
if (IS_ERR(gpiod))
|
|
return dev_err_probe(pse->dev, PTR_ERR(gpiod),
|
|
"failed to get reset gpio\n");
|
|
|
|
ret = rtpse_mcu_discover(pse, &info);
|
|
if (ret)
|
|
return ret;
|
|
|
|
/*
|
|
* Some boards gate all ports through a hardware line; deassert it only
|
|
* after the MCU is confirmed, so a discover failure never ungates the
|
|
* ports. It is then left to the MCU - not re-gated on unbind or a later
|
|
* probe error - so a driver reload doesn't black out PoE.
|
|
*/
|
|
gpiod = devm_gpiod_get_optional(pse->dev, "disable-ports", GPIOD_OUT_LOW);
|
|
if (IS_ERR(gpiod))
|
|
return dev_err_probe(pse->dev, PTR_ERR(gpiod),
|
|
"failed to get disable-ports gpio\n");
|
|
|
|
if (!info.system_enable) {
|
|
ret = rtpse_mcu_set_global_state(pse, true);
|
|
/* Dialects without a global-state concept (e.g. Gen1) return
|
|
* -EOPNOTSUPP; treat that as "no separate enable required".
|
|
*/
|
|
if (ret && ret != -EOPNOTSUPP)
|
|
return dev_err_probe(pse->dev, ret,
|
|
"failed to enable PSE system\n");
|
|
if (!ret) {
|
|
ret = devm_add_action_or_reset(pse->dev,
|
|
rtpse_mcu_global_disable, pse);
|
|
if (ret)
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Depending on the MCU firmware configuration (which might be different
|
|
* for every board), it isn't known whether the PoE subsystem is active or
|
|
* inactive by default. At this stage, the PSE chips might already deliver
|
|
* power to PDs without any explicit enable.
|
|
*/
|
|
|
|
/* pcdev.owner is set by the transport, so the registered controller
|
|
* pins the transport module that owns the live device, not the core.
|
|
*/
|
|
pse->pcdev.ops = &rtpse_mcu_ops;
|
|
pse->pcdev.dev = pse->dev;
|
|
pse->pcdev.types = ETHTOOL_PSE_C33;
|
|
pse->pcdev.nr_lines = info.max_ports;
|
|
pse->pcdev.pis_prio_max = RTPSE_MCU_PORT_MAX_PRIORITY;
|
|
pse->pcdev.supp_budget_eval_strategies = PSE_BUDGET_EVAL_STRAT_DYNAMIC;
|
|
|
|
return devm_pse_controller_register(pse->dev, &pse->pcdev);
|
|
}
|
|
EXPORT_SYMBOL_GPL(rtpse_mcu_register);
|
|
|
|
static void rtpse_mcu_gen2_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
|
|
{
|
|
info->max_ports = payload[1];
|
|
info->system_enable = (payload[2] == 0x1);
|
|
info->device_id = get_unaligned_be16(&payload[3]);
|
|
info->mcu_type = payload[6];
|
|
}
|
|
|
|
static int rtpse_mcu_gen2_parse_port_class(const struct rtpse_mcu_port_status *status)
|
|
{
|
|
/* Class lives in the upper nibble of sts2. */
|
|
return FIELD_GET(GENMASK(7, 4), status->sts2);
|
|
}
|
|
|
|
static const char *rtpse_mcu_gen2_mcu_type_str(unsigned int mcu_type)
|
|
{
|
|
switch (mcu_type) {
|
|
case 0x00: return "GigaDevice GD32F310";
|
|
case 0x01: return "GigaDevice GD32F230";
|
|
case 0x02: return "GigaDevice GD32F303";
|
|
case 0x03: return "GigaDevice GD32F103";
|
|
case 0x04: return "GigaDevice GD32E103";
|
|
case 0x10: return "Nuvoton M0516";
|
|
case 0x11: return "Nuvoton M0564";
|
|
case 0x12: return "Nuvoton NUC029";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
static void rtpse_mcu_gen1_parse_system_info(const u8 *payload, struct rtpse_mcu_info *info)
|
|
{
|
|
info->max_ports = payload[1];
|
|
/* Gen1 has no explicit system_enable byte; the closest analog is the
|
|
* "remote enable" bit in the system-status flags at payload[7].
|
|
*/
|
|
info->system_enable = !!(payload[7] & BIT(2));
|
|
info->device_id = get_unaligned_be16(&payload[3]);
|
|
info->mcu_type = payload[6];
|
|
}
|
|
|
|
static int rtpse_mcu_gen1_parse_port_class(const struct rtpse_mcu_port_status *status)
|
|
{
|
|
/* Gen1 puts the detected class in payload[3] (== sts3) directly.
|
|
* Mask to the low nibble; class is 0..8 and any high bits would be
|
|
* noise.
|
|
*/
|
|
return status->sts3 & 0x0f;
|
|
}
|
|
|
|
static const char *rtpse_mcu_gen1_mcu_type_str(unsigned int mcu_type)
|
|
{
|
|
switch (mcu_type) {
|
|
case 0x00: return "ST Micro ST32F100";
|
|
case 0x01: return "Nuvoton M05xx LAN";
|
|
case 0x02: return "ST Micro STF030C8";
|
|
case 0x03: return "Nuvoton M058SAN";
|
|
case 0x04: return "Nuvoton NUC122";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
/* Map each logical command the core issues to its per-dialect opcode. */
|
|
static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen2 = {
|
|
.parse_system_info = rtpse_mcu_gen2_parse_system_info,
|
|
.parse_port_class = rtpse_mcu_gen2_parse_port_class,
|
|
.mcu_type_str = rtpse_mcu_gen2_mcu_type_str,
|
|
.opcode = {
|
|
[RTPSE_MCU_CMD_SET_GLOBAL_STATE] = RTPSE_MCU_OP(0x00),
|
|
[RTPSE_MCU_CMD_GET_SYSTEM_INFO] = RTPSE_MCU_OP(0x40),
|
|
[RTPSE_MCU_CMD_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x4a),
|
|
|
|
[RTPSE_MCU_CMD_PORT_ENABLE] = RTPSE_MCU_OP(0x01),
|
|
[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x12),
|
|
[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT] = RTPSE_MCU_OP(0x13),
|
|
[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_EXT] = RTPSE_MCU_OP(0x14),
|
|
[RTPSE_MCU_CMD_PORT_SET_PRIORITY] = RTPSE_MCU_OP(0x15),
|
|
[RTPSE_MCU_CMD_PORT_GET_STATUS] = RTPSE_MCU_OP(0x42),
|
|
[RTPSE_MCU_CMD_PORT_GET_POWER_STATS] = RTPSE_MCU_OP(0x44),
|
|
[RTPSE_MCU_CMD_PORT_GET_CONFIG] = RTPSE_MCU_OP(0x48),
|
|
[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x49),
|
|
},
|
|
};
|
|
|
|
static const struct rtpse_mcu_dialect rtpse_mcu_dialect_gen1 = {
|
|
.parse_system_info = rtpse_mcu_gen1_parse_system_info,
|
|
.parse_port_class = rtpse_mcu_gen1_parse_port_class,
|
|
.mcu_type_str = rtpse_mcu_gen1_mcu_type_str,
|
|
.opcode = {
|
|
[RTPSE_MCU_CMD_GET_SYSTEM_INFO] = RTPSE_MCU_OP(0x20),
|
|
[RTPSE_MCU_CMD_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x2b),
|
|
|
|
[RTPSE_MCU_CMD_PORT_ENABLE] = RTPSE_MCU_OP(0x00),
|
|
[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT_TYPE] = RTPSE_MCU_OP(0x15),
|
|
[RTPSE_MCU_CMD_PORT_SET_POWER_LIMIT] = RTPSE_MCU_OP(0x16),
|
|
[RTPSE_MCU_CMD_PORT_SET_PRIORITY] = RTPSE_MCU_OP(0x1a),
|
|
[RTPSE_MCU_CMD_PORT_GET_STATUS] = RTPSE_MCU_OP(0x21),
|
|
[RTPSE_MCU_CMD_PORT_GET_POWER_STATS] = RTPSE_MCU_OP(0x30),
|
|
[RTPSE_MCU_CMD_PORT_GET_CONFIG] = RTPSE_MCU_OP(0x25),
|
|
[RTPSE_MCU_CMD_PORT_GET_EXT_CONFIG] = RTPSE_MCU_OP(0x26),
|
|
},
|
|
};
|
|
|
|
const struct rtpse_mcu_match_data rtpse_mcu_gen1_data = {
|
|
.dialect = &rtpse_mcu_dialect_gen1,
|
|
};
|
|
EXPORT_SYMBOL_GPL(rtpse_mcu_gen1_data);
|
|
|
|
const struct rtpse_mcu_match_data rtpse_mcu_gen2_data = {
|
|
.dialect = &rtpse_mcu_dialect_gen2,
|
|
};
|
|
EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_data);
|
|
|
|
/* Same dialect as gen2, but the MCU expects raw-I2C framing. */
|
|
const struct rtpse_mcu_match_data rtpse_mcu_gen2_i2c_data = {
|
|
.dialect = &rtpse_mcu_dialect_gen2,
|
|
.native_i2c = true,
|
|
};
|
|
EXPORT_SYMBOL_GPL(rtpse_mcu_gen2_i2c_data);
|
|
|
|
MODULE_AUTHOR("Jonas Jelonek <jelonek.jonas@gmail.com>");
|
|
MODULE_DESCRIPTION("Realtek PSE MCU driver (core)");
|
|
MODULE_LICENSE("GPL");
|