Commit 8c933eab authored by David S. Miller's avatar David S. Miller

Merge branch 'mlxsw-Make-port-split-code-more-generic'

Ido Schimmel says:

====================
mlxsw: Make port split code more generic

Jiri says:

Currently, we assume some limitations and constant values which are not
applicable for Spectrum-3 which has 8 lanes ports (instead of previous 4
lanes).

This patch does 2 things:

1) Generalizes the code to not use constants so it can work for 4, 8 and
   possibly 16 lanes.

2) Enforces some assumptions we had in the code but did not check.
====================
Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parents d74361dc 973b7fdb
...@@ -2017,6 +2017,35 @@ mlxsw_core_port_devlink_port_get(struct mlxsw_core *mlxsw_core, ...@@ -2017,6 +2017,35 @@ mlxsw_core_port_devlink_port_get(struct mlxsw_core *mlxsw_core,
} }
EXPORT_SYMBOL(mlxsw_core_port_devlink_port_get); EXPORT_SYMBOL(mlxsw_core_port_devlink_port_get);
int mlxsw_core_module_max_width(struct mlxsw_core *mlxsw_core, u8 module)
{
enum mlxsw_reg_pmtm_module_type module_type;
char pmtm_pl[MLXSW_REG_PMTM_LEN];
int err;
mlxsw_reg_pmtm_pack(pmtm_pl, module);
err = mlxsw_reg_query(mlxsw_core, MLXSW_REG(pmtm), pmtm_pl);
if (err)
return err;
mlxsw_reg_pmtm_unpack(pmtm_pl, &module_type);
/* Here we need to get the module width according to the module type. */
switch (module_type) {
case MLXSW_REG_PMTM_MODULE_TYPE_BP_4X: /* fall through */
case MLXSW_REG_PMTM_MODULE_TYPE_BP_QSFP:
return 4;
case MLXSW_REG_PMTM_MODULE_TYPE_BP_2X:
return 2;
case MLXSW_REG_PMTM_MODULE_TYPE_BP_SFP: /* fall through */
case MLXSW_REG_PMTM_MODULE_TYPE_BP_1X:
return 1;
default:
return -EINVAL;
}
}
EXPORT_SYMBOL(mlxsw_core_module_max_width);
static void mlxsw_core_buf_dump_dbg(struct mlxsw_core *mlxsw_core, static void mlxsw_core_buf_dump_dbg(struct mlxsw_core *mlxsw_core,
const char *buf, size_t size) const char *buf, size_t size)
{ {
......
...@@ -200,6 +200,7 @@ enum devlink_port_type mlxsw_core_port_type_get(struct mlxsw_core *mlxsw_core, ...@@ -200,6 +200,7 @@ enum devlink_port_type mlxsw_core_port_type_get(struct mlxsw_core *mlxsw_core,
struct devlink_port * struct devlink_port *
mlxsw_core_port_devlink_port_get(struct mlxsw_core *mlxsw_core, mlxsw_core_port_devlink_port_get(struct mlxsw_core *mlxsw_core,
u8 local_port); u8 local_port);
int mlxsw_core_module_max_width(struct mlxsw_core *mlxsw_core, u8 module);
int mlxsw_core_schedule_dw(struct delayed_work *dwork, unsigned long delay); int mlxsw_core_schedule_dw(struct delayed_work *dwork, unsigned long delay);
bool mlxsw_core_schedule_work(struct work_struct *work); bool mlxsw_core_schedule_work(struct work_struct *work);
......
...@@ -24,8 +24,6 @@ ...@@ -24,8 +24,6 @@
#define MLXSW_PORT_DONT_CARE 0xFF #define MLXSW_PORT_DONT_CARE 0xFF
#define MLXSW_PORT_MODULE_MAX_WIDTH 4
enum mlxsw_port_admin_status { enum mlxsw_port_admin_status {
MLXSW_PORT_ADMIN_STATUS_UP = 1, MLXSW_PORT_ADMIN_STATUS_UP = 1,
MLXSW_PORT_ADMIN_STATUS_DOWN = 2, MLXSW_PORT_ADMIN_STATUS_DOWN = 2,
......
...@@ -3969,6 +3969,7 @@ MLXSW_ITEM32(reg, pmlp, local_port, 0x00, 16, 8); ...@@ -3969,6 +3969,7 @@ MLXSW_ITEM32(reg, pmlp, local_port, 0x00, 16, 8);
* 1 - Lane 0 is used. * 1 - Lane 0 is used.
* 2 - Lanes 0 and 1 are used. * 2 - Lanes 0 and 1 are used.
* 4 - Lanes 0, 1, 2 and 3 are used. * 4 - Lanes 0, 1, 2 and 3 are used.
* 8 - Lanes 0-7 are used.
* Access: RW * Access: RW
*/ */
MLXSW_ITEM32(reg, pmlp, width, 0x00, 0, 8); MLXSW_ITEM32(reg, pmlp, width, 0x00, 0, 8);
...@@ -3983,14 +3984,14 @@ MLXSW_ITEM32_INDEXED(reg, pmlp, module, 0x04, 0, 8, 0x04, 0x00, false); ...@@ -3983,14 +3984,14 @@ MLXSW_ITEM32_INDEXED(reg, pmlp, module, 0x04, 0, 8, 0x04, 0x00, false);
* Tx Lane. When rxtx field is cleared, this field is used for Rx as well. * Tx Lane. When rxtx field is cleared, this field is used for Rx as well.
* Access: RW * Access: RW
*/ */
MLXSW_ITEM32_INDEXED(reg, pmlp, tx_lane, 0x04, 16, 2, 0x04, 0x00, false); MLXSW_ITEM32_INDEXED(reg, pmlp, tx_lane, 0x04, 16, 4, 0x04, 0x00, false);
/* reg_pmlp_rx_lane /* reg_pmlp_rx_lane
* Rx Lane. When rxtx field is cleared, this field is ignored and Rx lane is * Rx Lane. When rxtx field is cleared, this field is ignored and Rx lane is
* equal to Tx lane. * equal to Tx lane.
* Access: RW * Access: RW
*/ */
MLXSW_ITEM32_INDEXED(reg, pmlp, rx_lane, 0x04, 24, 2, 0x04, 0x00, false); MLXSW_ITEM32_INDEXED(reg, pmlp, rx_lane, 0x04, 24, 4, 0x04, 0x00, false);
static inline void mlxsw_reg_pmlp_pack(char *payload, u8 local_port) static inline void mlxsw_reg_pmlp_pack(char *payload, u8 local_port)
{ {
...@@ -5374,6 +5375,55 @@ static inline void mlxsw_reg_pplr_pack(char *payload, u8 local_port, ...@@ -5374,6 +5375,55 @@ static inline void mlxsw_reg_pplr_pack(char *payload, u8 local_port,
MLXSW_REG_PPLR_LB_TYPE_BIT_PHY_LOCAL : 0); MLXSW_REG_PPLR_LB_TYPE_BIT_PHY_LOCAL : 0);
} }
/* PMTM - Port Module Type Mapping Register
* ----------------------------------------
* The PMTM allows query or configuration of module types.
*/
#define MLXSW_REG_PMTM_ID 0x5067
#define MLXSW_REG_PMTM_LEN 0x10
MLXSW_REG_DEFINE(pmtm, MLXSW_REG_PMTM_ID, MLXSW_REG_PMTM_LEN);
/* reg_pmtm_module
* Module number.
* Access: Index
*/
MLXSW_ITEM32(reg, pmtm, module, 0x00, 16, 8);
enum mlxsw_reg_pmtm_module_type {
/* Backplane with 4 lanes */
MLXSW_REG_PMTM_MODULE_TYPE_BP_4X,
/* QSFP */
MLXSW_REG_PMTM_MODULE_TYPE_BP_QSFP,
/* SFP */
MLXSW_REG_PMTM_MODULE_TYPE_BP_SFP,
/* Backplane with single lane */
MLXSW_REG_PMTM_MODULE_TYPE_BP_1X = 4,
/* Backplane with two lane */
MLXSW_REG_PMTM_MODULE_TYPE_BP_2X = 8,
/* Chip2Chip */
MLXSW_REG_PMTM_MODULE_TYPE_C2C = 10,
};
/* reg_pmtm_module_type
* Module type.
* Access: RW
*/
MLXSW_ITEM32(reg, pmtm, module_type, 0x04, 0, 4);
static inline void mlxsw_reg_pmtm_pack(char *payload, u8 module)
{
MLXSW_REG_ZERO(pmtm, payload);
mlxsw_reg_pmtm_module_set(payload, module);
}
static inline void
mlxsw_reg_pmtm_unpack(char *payload,
enum mlxsw_reg_pmtm_module_type *module_type)
{
*module_type = mlxsw_reg_pmtm_module_type_get(payload);
}
/* HTGT - Host Trap Group Table /* HTGT - Host Trap Group Table
* ---------------------------- * ----------------------------
* Configures the properties for forwarding to CPU. * Configures the properties for forwarding to CPU.
...@@ -10544,6 +10594,7 @@ static const struct mlxsw_reg_info *mlxsw_reg_infos[] = { ...@@ -10544,6 +10594,7 @@ static const struct mlxsw_reg_info *mlxsw_reg_infos[] = {
MLXSW_REG(pbmc), MLXSW_REG(pbmc),
MLXSW_REG(pspa), MLXSW_REG(pspa),
MLXSW_REG(pplr), MLXSW_REG(pplr),
MLXSW_REG(pmtm),
MLXSW_REG(htgt), MLXSW_REG(htgt),
MLXSW_REG(hpkt), MLXSW_REG(hpkt),
MLXSW_REG(rgcr), MLXSW_REG(rgcr),
......
...@@ -26,6 +26,7 @@ enum mlxsw_res_id { ...@@ -26,6 +26,7 @@ enum mlxsw_res_id {
MLXSW_RES_ID_MAX_LAG_MEMBERS, MLXSW_RES_ID_MAX_LAG_MEMBERS,
MLXSW_RES_ID_LOCAL_PORTS_IN_1X, MLXSW_RES_ID_LOCAL_PORTS_IN_1X,
MLXSW_RES_ID_LOCAL_PORTS_IN_2X, MLXSW_RES_ID_LOCAL_PORTS_IN_2X,
MLXSW_RES_ID_LOCAL_PORTS_IN_4X,
MLXSW_RES_ID_GUARANTEED_SHARED_BUFFER, MLXSW_RES_ID_GUARANTEED_SHARED_BUFFER,
MLXSW_RES_ID_CELL_SIZE, MLXSW_RES_ID_CELL_SIZE,
MLXSW_RES_ID_MAX_HEADROOM_SIZE, MLXSW_RES_ID_MAX_HEADROOM_SIZE,
...@@ -82,6 +83,7 @@ static u16 mlxsw_res_ids[] = { ...@@ -82,6 +83,7 @@ static u16 mlxsw_res_ids[] = {
[MLXSW_RES_ID_MAX_LAG_MEMBERS] = 0x2521, [MLXSW_RES_ID_MAX_LAG_MEMBERS] = 0x2521,
[MLXSW_RES_ID_LOCAL_PORTS_IN_1X] = 0x2610, [MLXSW_RES_ID_LOCAL_PORTS_IN_1X] = 0x2610,
[MLXSW_RES_ID_LOCAL_PORTS_IN_2X] = 0x2611, [MLXSW_RES_ID_LOCAL_PORTS_IN_2X] = 0x2611,
[MLXSW_RES_ID_LOCAL_PORTS_IN_4X] = 0x2612,
[MLXSW_RES_ID_GUARANTEED_SHARED_BUFFER] = 0x2805, /* Bytes */ [MLXSW_RES_ID_GUARANTEED_SHARED_BUFFER] = 0x2805, /* Bytes */
[MLXSW_RES_ID_CELL_SIZE] = 0x2803, /* Bytes */ [MLXSW_RES_ID_CELL_SIZE] = 0x2803, /* Bytes */
[MLXSW_RES_ID_MAX_HEADROOM_SIZE] = 0x2811, /* Bytes */ [MLXSW_RES_ID_MAX_HEADROOM_SIZE] = 0x2811, /* Bytes */
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
#include <linux/inetdevice.h> #include <linux/inetdevice.h>
#include <linux/netlink.h> #include <linux/netlink.h>
#include <linux/jhash.h> #include <linux/jhash.h>
#include <linux/log2.h>
#include <net/switchdev.h> #include <net/switchdev.h>
#include <net/pkt_cls.h> #include <net/pkt_cls.h>
#include <net/tc_act/tc_mirred.h> #include <net/tc_act/tc_mirred.h>
...@@ -748,35 +749,69 @@ mlxsw_sp_port_system_port_mapping_set(struct mlxsw_sp_port *mlxsw_sp_port) ...@@ -748,35 +749,69 @@ mlxsw_sp_port_system_port_mapping_set(struct mlxsw_sp_port *mlxsw_sp_port)
return mlxsw_reg_write(mlxsw_sp->core, MLXSW_REG(sspr), sspr_pl); return mlxsw_reg_write(mlxsw_sp->core, MLXSW_REG(sspr), sspr_pl);
} }
static int mlxsw_sp_port_module_info_get(struct mlxsw_sp *mlxsw_sp, static int
u8 local_port, u8 *p_module, mlxsw_sp_port_module_info_get(struct mlxsw_sp *mlxsw_sp, u8 local_port,
u8 *p_width, u8 *p_lane) struct mlxsw_sp_port_mapping *port_mapping)
{ {
char pmlp_pl[MLXSW_REG_PMLP_LEN]; char pmlp_pl[MLXSW_REG_PMLP_LEN];
bool separate_rxtx;
u8 module;
u8 width;
int err; int err;
int i;
mlxsw_reg_pmlp_pack(pmlp_pl, local_port); mlxsw_reg_pmlp_pack(pmlp_pl, local_port);
err = mlxsw_reg_query(mlxsw_sp->core, MLXSW_REG(pmlp), pmlp_pl); err = mlxsw_reg_query(mlxsw_sp->core, MLXSW_REG(pmlp), pmlp_pl);
if (err) if (err)
return err; return err;
*p_module = mlxsw_reg_pmlp_module_get(pmlp_pl, 0); module = mlxsw_reg_pmlp_module_get(pmlp_pl, 0);
*p_width = mlxsw_reg_pmlp_width_get(pmlp_pl); width = mlxsw_reg_pmlp_width_get(pmlp_pl);
*p_lane = mlxsw_reg_pmlp_tx_lane_get(pmlp_pl, 0); separate_rxtx = mlxsw_reg_pmlp_rxtx_get(pmlp_pl);
if (width && !is_power_of_2(width)) {
dev_err(mlxsw_sp->bus_info->dev, "Port %d: Unsupported module config: width value is not power of 2\n",
local_port);
return -EINVAL;
}
for (i = 0; i < width; i++) {
if (mlxsw_reg_pmlp_module_get(pmlp_pl, i) != module) {
dev_err(mlxsw_sp->bus_info->dev, "Port %d: Unsupported module config: contains multiple modules\n",
local_port);
return -EINVAL;
}
if (separate_rxtx &&
mlxsw_reg_pmlp_tx_lane_get(pmlp_pl, i) !=
mlxsw_reg_pmlp_rx_lane_get(pmlp_pl, i)) {
dev_err(mlxsw_sp->bus_info->dev, "Port %d: Unsupported module config: TX and RX lane numbers are different\n",
local_port);
return -EINVAL;
}
if (mlxsw_reg_pmlp_tx_lane_get(pmlp_pl, i) != i) {
dev_err(mlxsw_sp->bus_info->dev, "Port %d: Unsupported module config: TX and RX lane numbers are not sequential\n",
local_port);
return -EINVAL;
}
}
port_mapping->module = module;
port_mapping->width = width;
port_mapping->lane = mlxsw_reg_pmlp_tx_lane_get(pmlp_pl, 0);
return 0; return 0;
} }
static int mlxsw_sp_port_module_map(struct mlxsw_sp_port *mlxsw_sp_port, static int mlxsw_sp_port_module_map(struct mlxsw_sp_port *mlxsw_sp_port)
u8 module, u8 width, u8 lane)
{ {
struct mlxsw_sp_port_mapping *port_mapping = &mlxsw_sp_port->mapping;
struct mlxsw_sp *mlxsw_sp = mlxsw_sp_port->mlxsw_sp; struct mlxsw_sp *mlxsw_sp = mlxsw_sp_port->mlxsw_sp;
char pmlp_pl[MLXSW_REG_PMLP_LEN]; char pmlp_pl[MLXSW_REG_PMLP_LEN];
int i; int i;
mlxsw_reg_pmlp_pack(pmlp_pl, mlxsw_sp_port->local_port); mlxsw_reg_pmlp_pack(pmlp_pl, mlxsw_sp_port->local_port);
mlxsw_reg_pmlp_width_set(pmlp_pl, width); mlxsw_reg_pmlp_width_set(pmlp_pl, port_mapping->width);
for (i = 0; i < width; i++) { for (i = 0; i < port_mapping->width; i++) {
mlxsw_reg_pmlp_module_set(pmlp_pl, i, module); mlxsw_reg_pmlp_module_set(pmlp_pl, i, port_mapping->module);
mlxsw_reg_pmlp_tx_lane_set(pmlp_pl, i, lane + i); /* Rx & Tx */ mlxsw_reg_pmlp_tx_lane_set(pmlp_pl, i, port_mapping->lane + i); /* Rx & Tx */
} }
return mlxsw_reg_write(mlxsw_sp->core, MLXSW_REG(pmlp), pmlp_pl); return mlxsw_reg_write(mlxsw_sp->core, MLXSW_REG(pmlp), pmlp_pl);
...@@ -3480,7 +3515,7 @@ static const struct ethtool_ops mlxsw_sp_port_ethtool_ops = { ...@@ -3480,7 +3515,7 @@ static const struct ethtool_ops mlxsw_sp_port_ethtool_ops = {
}; };
static int static int
mlxsw_sp_port_speed_by_width_set(struct mlxsw_sp_port *mlxsw_sp_port, u8 width) mlxsw_sp_port_speed_by_width_set(struct mlxsw_sp_port *mlxsw_sp_port)
{ {
struct mlxsw_sp *mlxsw_sp = mlxsw_sp_port->mlxsw_sp; struct mlxsw_sp *mlxsw_sp = mlxsw_sp_port->mlxsw_sp;
const struct mlxsw_sp_port_type_speed_ops *ops; const struct mlxsw_sp_port_type_speed_ops *ops;
...@@ -3496,7 +3531,7 @@ mlxsw_sp_port_speed_by_width_set(struct mlxsw_sp_port *mlxsw_sp_port, u8 width) ...@@ -3496,7 +3531,7 @@ mlxsw_sp_port_speed_by_width_set(struct mlxsw_sp_port *mlxsw_sp_port, u8 width)
&base_speed); &base_speed);
if (err) if (err)
return err; return err;
upper_speed = base_speed * width; upper_speed = base_speed * mlxsw_sp_port->mapping.width;
eth_proto_admin = ops->to_ptys_upper_speed(mlxsw_sp, upper_speed); eth_proto_admin = ops->to_ptys_upper_speed(mlxsw_sp, upper_speed);
ops->reg_ptys_eth_pack(mlxsw_sp, ptys_pl, mlxsw_sp_port->local_port, ops->reg_ptys_eth_pack(mlxsw_sp, ptys_pl, mlxsw_sp_port->local_port,
...@@ -3657,15 +3692,18 @@ static int mlxsw_sp_port_tc_mc_mode_set(struct mlxsw_sp_port *mlxsw_sp_port, ...@@ -3657,15 +3692,18 @@ static int mlxsw_sp_port_tc_mc_mode_set(struct mlxsw_sp_port *mlxsw_sp_port,
} }
static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port, static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port,
bool split, u8 module, u8 width, u8 lane) u8 split_base_local_port,
struct mlxsw_sp_port_mapping *port_mapping)
{ {
struct mlxsw_sp_port_vlan *mlxsw_sp_port_vlan; struct mlxsw_sp_port_vlan *mlxsw_sp_port_vlan;
bool split = !!split_base_local_port;
struct mlxsw_sp_port *mlxsw_sp_port; struct mlxsw_sp_port *mlxsw_sp_port;
struct net_device *dev; struct net_device *dev;
int err; int err;
err = mlxsw_core_port_init(mlxsw_sp->core, local_port, err = mlxsw_core_port_init(mlxsw_sp->core, local_port,
module + 1, split, lane / width, port_mapping->module + 1, split,
port_mapping->lane / port_mapping->width,
mlxsw_sp->base_mac, mlxsw_sp->base_mac,
sizeof(mlxsw_sp->base_mac)); sizeof(mlxsw_sp->base_mac));
if (err) { if (err) {
...@@ -3687,9 +3725,8 @@ static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port, ...@@ -3687,9 +3725,8 @@ static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port,
mlxsw_sp_port->local_port = local_port; mlxsw_sp_port->local_port = local_port;
mlxsw_sp_port->pvid = MLXSW_SP_DEFAULT_VID; mlxsw_sp_port->pvid = MLXSW_SP_DEFAULT_VID;
mlxsw_sp_port->split = split; mlxsw_sp_port->split = split;
mlxsw_sp_port->mapping.module = module; mlxsw_sp_port->split_base_local_port = split_base_local_port;
mlxsw_sp_port->mapping.width = width; mlxsw_sp_port->mapping = *port_mapping;
mlxsw_sp_port->mapping.lane = lane;
mlxsw_sp_port->link.autoneg = 1; mlxsw_sp_port->link.autoneg = 1;
INIT_LIST_HEAD(&mlxsw_sp_port->vlans_list); INIT_LIST_HEAD(&mlxsw_sp_port->vlans_list);
INIT_LIST_HEAD(&mlxsw_sp_port->mall_tc_list); INIT_LIST_HEAD(&mlxsw_sp_port->mall_tc_list);
...@@ -3714,7 +3751,7 @@ static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port, ...@@ -3714,7 +3751,7 @@ static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port,
dev->netdev_ops = &mlxsw_sp_port_netdev_ops; dev->netdev_ops = &mlxsw_sp_port_netdev_ops;
dev->ethtool_ops = &mlxsw_sp_port_ethtool_ops; dev->ethtool_ops = &mlxsw_sp_port_ethtool_ops;
err = mlxsw_sp_port_module_map(mlxsw_sp_port, module, width, lane); err = mlxsw_sp_port_module_map(mlxsw_sp_port);
if (err) { if (err) {
dev_err(mlxsw_sp->bus_info->dev, "Port %d: Failed to map module\n", dev_err(mlxsw_sp->bus_info->dev, "Port %d: Failed to map module\n",
mlxsw_sp_port->local_port); mlxsw_sp_port->local_port);
...@@ -3756,7 +3793,7 @@ static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port, ...@@ -3756,7 +3793,7 @@ static int mlxsw_sp_port_create(struct mlxsw_sp *mlxsw_sp, u8 local_port,
goto err_port_system_port_mapping_set; goto err_port_system_port_mapping_set;
} }
err = mlxsw_sp_port_speed_by_width_set(mlxsw_sp_port, width); err = mlxsw_sp_port_speed_by_width_set(mlxsw_sp_port);
if (err) { if (err) {
dev_err(mlxsw_sp->bus_info->dev, "Port %d: Failed to enable speeds\n", dev_err(mlxsw_sp->bus_info->dev, "Port %d: Failed to enable speeds\n",
mlxsw_sp_port->local_port); mlxsw_sp_port->local_port);
...@@ -3979,14 +4016,13 @@ static void mlxsw_sp_ports_remove(struct mlxsw_sp *mlxsw_sp) ...@@ -3979,14 +4016,13 @@ static void mlxsw_sp_ports_remove(struct mlxsw_sp *mlxsw_sp)
if (mlxsw_sp_port_created(mlxsw_sp, i)) if (mlxsw_sp_port_created(mlxsw_sp, i))
mlxsw_sp_port_remove(mlxsw_sp, i); mlxsw_sp_port_remove(mlxsw_sp, i);
mlxsw_sp_cpu_port_remove(mlxsw_sp); mlxsw_sp_cpu_port_remove(mlxsw_sp);
kfree(mlxsw_sp->port_to_module);
kfree(mlxsw_sp->ports); kfree(mlxsw_sp->ports);
} }
static int mlxsw_sp_ports_create(struct mlxsw_sp *mlxsw_sp) static int mlxsw_sp_ports_create(struct mlxsw_sp *mlxsw_sp)
{ {
unsigned int max_ports = mlxsw_core_max_ports(mlxsw_sp->core); unsigned int max_ports = mlxsw_core_max_ports(mlxsw_sp->core);
u8 module, width, lane; struct mlxsw_sp_port_mapping *port_mapping;
size_t alloc_size; size_t alloc_size;
int i; int i;
int err; int err;
...@@ -3996,66 +4032,98 @@ static int mlxsw_sp_ports_create(struct mlxsw_sp *mlxsw_sp) ...@@ -3996,66 +4032,98 @@ static int mlxsw_sp_ports_create(struct mlxsw_sp *mlxsw_sp)
if (!mlxsw_sp->ports) if (!mlxsw_sp->ports)
return -ENOMEM; return -ENOMEM;
mlxsw_sp->port_to_module = kmalloc_array(max_ports, sizeof(int),
GFP_KERNEL);
if (!mlxsw_sp->port_to_module) {
err = -ENOMEM;
goto err_port_to_module_alloc;
}
err = mlxsw_sp_cpu_port_create(mlxsw_sp); err = mlxsw_sp_cpu_port_create(mlxsw_sp);
if (err) if (err)
goto err_cpu_port_create; goto err_cpu_port_create;
for (i = 1; i < max_ports; i++) { for (i = 1; i < max_ports; i++) {
/* Mark as invalid */ port_mapping = mlxsw_sp->port_mapping[i];
mlxsw_sp->port_to_module[i] = -1; if (!port_mapping)
err = mlxsw_sp_port_module_info_get(mlxsw_sp, i, &module,
&width, &lane);
if (err)
goto err_port_module_info_get;
if (!width)
continue; continue;
mlxsw_sp->port_to_module[i] = module; err = mlxsw_sp_port_create(mlxsw_sp, i, 0, port_mapping);
err = mlxsw_sp_port_create(mlxsw_sp, i, false,
module, width, lane);
if (err) if (err)
goto err_port_create; goto err_port_create;
} }
return 0; return 0;
err_port_create: err_port_create:
err_port_module_info_get:
for (i--; i >= 1; i--) for (i--; i >= 1; i--)
if (mlxsw_sp_port_created(mlxsw_sp, i)) if (mlxsw_sp_port_created(mlxsw_sp, i))
mlxsw_sp_port_remove(mlxsw_sp, i); mlxsw_sp_port_remove(mlxsw_sp, i);
mlxsw_sp_cpu_port_remove(mlxsw_sp); mlxsw_sp_cpu_port_remove(mlxsw_sp);
err_cpu_port_create: err_cpu_port_create:
kfree(mlxsw_sp->port_to_module);
err_port_to_module_alloc:
kfree(mlxsw_sp->ports); kfree(mlxsw_sp->ports);
return err; return err;
} }
static u8 mlxsw_sp_cluster_base_port_get(u8 local_port) static int mlxsw_sp_port_module_info_init(struct mlxsw_sp *mlxsw_sp)
{ {
u8 offset = (local_port - 1) % MLXSW_SP_PORTS_PER_CLUSTER_MAX; unsigned int max_ports = mlxsw_core_max_ports(mlxsw_sp->core);
struct mlxsw_sp_port_mapping port_mapping;
int i;
int err;
mlxsw_sp->port_mapping = kcalloc(max_ports,
sizeof(struct mlxsw_sp_port_mapping *),
GFP_KERNEL);
if (!mlxsw_sp->port_mapping)
return -ENOMEM;
for (i = 1; i < max_ports; i++) {
err = mlxsw_sp_port_module_info_get(mlxsw_sp, i, &port_mapping);
if (err)
goto err_port_module_info_get;
if (!port_mapping.width)
continue;
mlxsw_sp->port_mapping[i] = kmemdup(&port_mapping,
sizeof(port_mapping),
GFP_KERNEL);
if (!mlxsw_sp->port_mapping[i])
goto err_port_module_info_dup;
}
return 0;
err_port_module_info_get:
err_port_module_info_dup:
for (i--; i >= 1; i--)
kfree(mlxsw_sp->port_mapping[i]);
kfree(mlxsw_sp->port_mapping);
return err;
}
static void mlxsw_sp_port_module_info_fini(struct mlxsw_sp *mlxsw_sp)
{
int i;
for (i = 1; i < mlxsw_core_max_ports(mlxsw_sp->core); i++)
kfree(mlxsw_sp->port_mapping[i]);
kfree(mlxsw_sp->port_mapping);
}
static u8 mlxsw_sp_cluster_base_port_get(u8 local_port, unsigned int max_width)
{
u8 offset = (local_port - 1) % max_width;
return local_port - offset; return local_port - offset;
} }
static int mlxsw_sp_port_split_create(struct mlxsw_sp *mlxsw_sp, u8 base_port, static int
u8 module, unsigned int count, u8 offset) mlxsw_sp_port_split_create(struct mlxsw_sp *mlxsw_sp, u8 base_port,
struct mlxsw_sp_port_mapping *port_mapping,
unsigned int count, u8 offset)
{ {
u8 width = MLXSW_PORT_MODULE_MAX_WIDTH / count; struct mlxsw_sp_port_mapping split_port_mapping;
int err, i; int err, i;
split_port_mapping = *port_mapping;
split_port_mapping.width /= count;
for (i = 0; i < count; i++) { for (i = 0; i < count; i++) {
err = mlxsw_sp_port_create(mlxsw_sp, base_port + i * offset, err = mlxsw_sp_port_create(mlxsw_sp, base_port + i * offset,
true, module, width, i * width); base_port, &split_port_mapping);
if (err) if (err)
goto err_port_create; goto err_port_create;
split_port_mapping.lane += split_port_mapping.width;
} }
return 0; return 0;
...@@ -4068,45 +4136,55 @@ static int mlxsw_sp_port_split_create(struct mlxsw_sp *mlxsw_sp, u8 base_port, ...@@ -4068,45 +4136,55 @@ static int mlxsw_sp_port_split_create(struct mlxsw_sp *mlxsw_sp, u8 base_port,
} }
static void mlxsw_sp_port_unsplit_create(struct mlxsw_sp *mlxsw_sp, static void mlxsw_sp_port_unsplit_create(struct mlxsw_sp *mlxsw_sp,
u8 base_port, unsigned int count) u8 base_port,
unsigned int count, u8 offset)
{ {
u8 local_port, module, width = MLXSW_PORT_MODULE_MAX_WIDTH; struct mlxsw_sp_port_mapping *port_mapping;
int i; int i;
/* Split by four means we need to re-create two ports, otherwise /* Go over original unsplit ports in the gap and recreate them. */
* only one. for (i = 0; i < count * offset; i++) {
*/ port_mapping = mlxsw_sp->port_mapping[base_port + i];
count = count / 2; if (!port_mapping)
for (i = 0; i < count; i++) {
local_port = base_port + i * 2;
if (mlxsw_sp->port_to_module[local_port] < 0)
continue; continue;
module = mlxsw_sp->port_to_module[local_port]; mlxsw_sp_port_create(mlxsw_sp, base_port + i, 0, port_mapping);
mlxsw_sp_port_create(mlxsw_sp, local_port, false, module,
width, 0);
} }
} }
static int mlxsw_sp_local_ports_offset(struct mlxsw_core *mlxsw_core,
unsigned int count,
unsigned int max_width)
{
enum mlxsw_res_id local_ports_in_x_res_id;
int split_width = max_width / count;
if (split_width == 1)
local_ports_in_x_res_id = MLXSW_RES_ID_LOCAL_PORTS_IN_1X;
else if (split_width == 2)
local_ports_in_x_res_id = MLXSW_RES_ID_LOCAL_PORTS_IN_2X;
else if (split_width == 4)
local_ports_in_x_res_id = MLXSW_RES_ID_LOCAL_PORTS_IN_4X;
else
return -EINVAL;
if (!mlxsw_core_res_valid(mlxsw_core, local_ports_in_x_res_id))
return -EINVAL;
return mlxsw_core_res_get(mlxsw_core, local_ports_in_x_res_id);
}
static int mlxsw_sp_port_split(struct mlxsw_core *mlxsw_core, u8 local_port, static int mlxsw_sp_port_split(struct mlxsw_core *mlxsw_core, u8 local_port,
unsigned int count, unsigned int count,
struct netlink_ext_ack *extack) struct netlink_ext_ack *extack)
{ {
struct mlxsw_sp *mlxsw_sp = mlxsw_core_driver_priv(mlxsw_core); struct mlxsw_sp *mlxsw_sp = mlxsw_core_driver_priv(mlxsw_core);
u8 local_ports_in_1x, local_ports_in_2x, offset; struct mlxsw_sp_port_mapping port_mapping;
struct mlxsw_sp_port *mlxsw_sp_port; struct mlxsw_sp_port *mlxsw_sp_port;
u8 module, cur_width, base_port; int max_width;
u8 base_port;
int offset;
int i; int i;
int err; int err;
if (!MLXSW_CORE_RES_VALID(mlxsw_core, LOCAL_PORTS_IN_1X) ||
!MLXSW_CORE_RES_VALID(mlxsw_core, LOCAL_PORTS_IN_2X))
return -EIO;
local_ports_in_1x = MLXSW_CORE_RES_GET(mlxsw_core, LOCAL_PORTS_IN_1X);
local_ports_in_2x = MLXSW_CORE_RES_GET(mlxsw_core, LOCAL_PORTS_IN_2X);
mlxsw_sp_port = mlxsw_sp->ports[local_port]; mlxsw_sp_port = mlxsw_sp->ports[local_port];
if (!mlxsw_sp_port) { if (!mlxsw_sp_port) {
dev_err(mlxsw_sp->bus_info->dev, "Port number \"%d\" does not exist\n", dev_err(mlxsw_sp->bus_info->dev, "Port number \"%d\" does not exist\n",
...@@ -4115,47 +4193,70 @@ static int mlxsw_sp_port_split(struct mlxsw_core *mlxsw_core, u8 local_port, ...@@ -4115,47 +4193,70 @@ static int mlxsw_sp_port_split(struct mlxsw_core *mlxsw_core, u8 local_port,
return -EINVAL; return -EINVAL;
} }
module = mlxsw_sp_port->mapping.module; /* Split ports cannot be split. */
cur_width = mlxsw_sp_port->mapping.width; if (mlxsw_sp_port->split) {
netdev_err(mlxsw_sp_port->dev, "Port cannot be split further\n");
NL_SET_ERR_MSG_MOD(extack, "Port cannot be split further");
return -EINVAL;
}
max_width = mlxsw_core_module_max_width(mlxsw_core,
mlxsw_sp_port->mapping.module);
if (max_width < 0) {
netdev_err(mlxsw_sp_port->dev, "Cannot get max width of port module\n");
NL_SET_ERR_MSG_MOD(extack, "Cannot get max width of port module");
return max_width;
}
if (count != 2 && count != 4) { /* Split port with non-max and 1 module width cannot be split. */
netdev_err(mlxsw_sp_port->dev, "Port can only be split into 2 or 4 ports\n"); if (mlxsw_sp_port->mapping.width != max_width || max_width == 1) {
NL_SET_ERR_MSG_MOD(extack, "Port can only be split into 2 or 4 ports"); netdev_err(mlxsw_sp_port->dev, "Port cannot be split\n");
NL_SET_ERR_MSG_MOD(extack, "Port cannot be split");
return -EINVAL; return -EINVAL;
} }
if (cur_width != MLXSW_PORT_MODULE_MAX_WIDTH) { if (count == 1 || !is_power_of_2(count) || count > max_width) {
netdev_err(mlxsw_sp_port->dev, "Port cannot be split further\n"); netdev_err(mlxsw_sp_port->dev, "Invalid split count\n");
NL_SET_ERR_MSG_MOD(extack, "Port cannot be split further"); NL_SET_ERR_MSG_MOD(extack, "Invalid split count");
return -EINVAL; return -EINVAL;
} }
/* Make sure we have enough slave (even) ports for the split. */ offset = mlxsw_sp_local_ports_offset(mlxsw_core, count, max_width);
if (count == 2) { if (offset < 0) {
offset = local_ports_in_2x; netdev_err(mlxsw_sp_port->dev, "Cannot obtain local port offset\n");
base_port = local_port; NL_SET_ERR_MSG_MOD(extack, "Cannot obtain local port offset");
if (mlxsw_sp->ports[base_port + local_ports_in_2x]) {
netdev_err(mlxsw_sp_port->dev, "Invalid split configuration\n");
NL_SET_ERR_MSG_MOD(extack, "Invalid split configuration");
return -EINVAL; return -EINVAL;
} }
} else {
offset = local_ports_in_1x; /* Only in case max split is being done, the local port and
base_port = mlxsw_sp_cluster_base_port_get(local_port); * base port may differ.
if (mlxsw_sp->ports[base_port + 1] || */
mlxsw_sp->ports[base_port + 3]) { base_port = count == max_width ?
mlxsw_sp_cluster_base_port_get(local_port, max_width) :
local_port;
for (i = 0; i < count * offset; i++) {
/* Expect base port to exist and also the one in the middle in
* case of maximal split count.
*/
if (i == 0 || (count == max_width && i == count / 2))
continue;
if (mlxsw_sp_port_created(mlxsw_sp, base_port + i)) {
netdev_err(mlxsw_sp_port->dev, "Invalid split configuration\n"); netdev_err(mlxsw_sp_port->dev, "Invalid split configuration\n");
NL_SET_ERR_MSG_MOD(extack, "Invalid split configuration"); NL_SET_ERR_MSG_MOD(extack, "Invalid split configuration");
return -EINVAL; return -EINVAL;
} }
} }
port_mapping = mlxsw_sp_port->mapping;
for (i = 0; i < count; i++) for (i = 0; i < count; i++)
if (mlxsw_sp_port_created(mlxsw_sp, base_port + i * offset)) if (mlxsw_sp_port_created(mlxsw_sp, base_port + i * offset))
mlxsw_sp_port_remove(mlxsw_sp, base_port + i * offset); mlxsw_sp_port_remove(mlxsw_sp, base_port + i * offset);
err = mlxsw_sp_port_split_create(mlxsw_sp, base_port, module, count, err = mlxsw_sp_port_split_create(mlxsw_sp, base_port, &port_mapping,
offset); count, offset);
if (err) { if (err) {
dev_err(mlxsw_sp->bus_info->dev, "Failed to create split ports\n"); dev_err(mlxsw_sp->bus_info->dev, "Failed to create split ports\n");
goto err_port_split_create; goto err_port_split_create;
...@@ -4164,7 +4265,7 @@ static int mlxsw_sp_port_split(struct mlxsw_core *mlxsw_core, u8 local_port, ...@@ -4164,7 +4265,7 @@ static int mlxsw_sp_port_split(struct mlxsw_core *mlxsw_core, u8 local_port,
return 0; return 0;
err_port_split_create: err_port_split_create:
mlxsw_sp_port_unsplit_create(mlxsw_sp, base_port, count); mlxsw_sp_port_unsplit_create(mlxsw_sp, base_port, count, offset);
return err; return err;
} }
...@@ -4172,19 +4273,13 @@ static int mlxsw_sp_port_unsplit(struct mlxsw_core *mlxsw_core, u8 local_port, ...@@ -4172,19 +4273,13 @@ static int mlxsw_sp_port_unsplit(struct mlxsw_core *mlxsw_core, u8 local_port,
struct netlink_ext_ack *extack) struct netlink_ext_ack *extack)
{ {
struct mlxsw_sp *mlxsw_sp = mlxsw_core_driver_priv(mlxsw_core); struct mlxsw_sp *mlxsw_sp = mlxsw_core_driver_priv(mlxsw_core);
u8 local_ports_in_1x, local_ports_in_2x, offset;
struct mlxsw_sp_port *mlxsw_sp_port; struct mlxsw_sp_port *mlxsw_sp_port;
u8 cur_width, base_port;
unsigned int count; unsigned int count;
int max_width;
u8 base_port;
int offset;
int i; int i;
if (!MLXSW_CORE_RES_VALID(mlxsw_core, LOCAL_PORTS_IN_1X) ||
!MLXSW_CORE_RES_VALID(mlxsw_core, LOCAL_PORTS_IN_2X))
return -EIO;
local_ports_in_1x = MLXSW_CORE_RES_GET(mlxsw_core, LOCAL_PORTS_IN_1X);
local_ports_in_2x = MLXSW_CORE_RES_GET(mlxsw_core, LOCAL_PORTS_IN_2X);
mlxsw_sp_port = mlxsw_sp->ports[local_port]; mlxsw_sp_port = mlxsw_sp->ports[local_port];
if (!mlxsw_sp_port) { if (!mlxsw_sp_port) {
dev_err(mlxsw_sp->bus_info->dev, "Port number \"%d\" does not exist\n", dev_err(mlxsw_sp->bus_info->dev, "Port number \"%d\" does not exist\n",
...@@ -4199,25 +4294,30 @@ static int mlxsw_sp_port_unsplit(struct mlxsw_core *mlxsw_core, u8 local_port, ...@@ -4199,25 +4294,30 @@ static int mlxsw_sp_port_unsplit(struct mlxsw_core *mlxsw_core, u8 local_port,
return -EINVAL; return -EINVAL;
} }
cur_width = mlxsw_sp_port->mapping.width; max_width = mlxsw_core_module_max_width(mlxsw_core,
count = cur_width == 1 ? 4 : 2; mlxsw_sp_port->mapping.module);
if (max_width < 0) {
netdev_err(mlxsw_sp_port->dev, "Cannot get max width of port module\n");
NL_SET_ERR_MSG_MOD(extack, "Cannot get max width of port module");
return max_width;
}
if (count == 2) count = max_width / mlxsw_sp_port->mapping.width;
offset = local_ports_in_2x;
else
offset = local_ports_in_1x;
base_port = mlxsw_sp_cluster_base_port_get(local_port); offset = mlxsw_sp_local_ports_offset(mlxsw_core, count, max_width);
if (WARN_ON(offset < 0)) {
netdev_err(mlxsw_sp_port->dev, "Cannot obtain local port offset\n");
NL_SET_ERR_MSG_MOD(extack, "Cannot obtain local port offset");
return -EINVAL;
}
/* Determine which ports to remove. */ base_port = mlxsw_sp_port->split_base_local_port;
if (count == 2 && local_port >= base_port + 2)
base_port = base_port + 2;
for (i = 0; i < count; i++) for (i = 0; i < count; i++)
if (mlxsw_sp_port_created(mlxsw_sp, base_port + i * offset)) if (mlxsw_sp_port_created(mlxsw_sp, base_port + i * offset))
mlxsw_sp_port_remove(mlxsw_sp, base_port + i * offset); mlxsw_sp_port_remove(mlxsw_sp, base_port + i * offset);
mlxsw_sp_port_unsplit_create(mlxsw_sp, base_port, count); mlxsw_sp_port_unsplit_create(mlxsw_sp, base_port, count, offset);
return 0; return 0;
} }
...@@ -4924,6 +5024,12 @@ static int mlxsw_sp_init(struct mlxsw_core *mlxsw_core, ...@@ -4924,6 +5024,12 @@ static int mlxsw_sp_init(struct mlxsw_core *mlxsw_core,
goto err_dpipe_init; goto err_dpipe_init;
} }
err = mlxsw_sp_port_module_info_init(mlxsw_sp);
if (err) {
dev_err(mlxsw_sp->bus_info->dev, "Failed to init port module info\n");
goto err_port_module_info_init;
}
err = mlxsw_sp_ports_create(mlxsw_sp); err = mlxsw_sp_ports_create(mlxsw_sp);
if (err) { if (err) {
dev_err(mlxsw_sp->bus_info->dev, "Failed to create ports\n"); dev_err(mlxsw_sp->bus_info->dev, "Failed to create ports\n");
...@@ -4933,6 +5039,8 @@ static int mlxsw_sp_init(struct mlxsw_core *mlxsw_core, ...@@ -4933,6 +5039,8 @@ static int mlxsw_sp_init(struct mlxsw_core *mlxsw_core,
return 0; return 0;
err_ports_create: err_ports_create:
mlxsw_sp_port_module_info_fini(mlxsw_sp);
err_port_module_info_init:
mlxsw_sp_dpipe_fini(mlxsw_sp); mlxsw_sp_dpipe_fini(mlxsw_sp);
err_dpipe_init: err_dpipe_init:
unregister_netdevice_notifier_net(mlxsw_sp_net(mlxsw_sp), unregister_netdevice_notifier_net(mlxsw_sp_net(mlxsw_sp),
...@@ -5025,6 +5133,7 @@ static void mlxsw_sp_fini(struct mlxsw_core *mlxsw_core) ...@@ -5025,6 +5133,7 @@ static void mlxsw_sp_fini(struct mlxsw_core *mlxsw_core)
struct mlxsw_sp *mlxsw_sp = mlxsw_core_driver_priv(mlxsw_core); struct mlxsw_sp *mlxsw_sp = mlxsw_core_driver_priv(mlxsw_core);
mlxsw_sp_ports_remove(mlxsw_sp); mlxsw_sp_ports_remove(mlxsw_sp);
mlxsw_sp_port_module_info_fini(mlxsw_sp);
mlxsw_sp_dpipe_fini(mlxsw_sp); mlxsw_sp_dpipe_fini(mlxsw_sp);
unregister_netdevice_notifier_net(mlxsw_sp_net(mlxsw_sp), unregister_netdevice_notifier_net(mlxsw_sp_net(mlxsw_sp),
&mlxsw_sp->netdevice_nb); &mlxsw_sp->netdevice_nb);
......
...@@ -32,8 +32,6 @@ ...@@ -32,8 +32,6 @@
#define MLXSW_SP_MID_MAX 7000 #define MLXSW_SP_MID_MAX 7000
#define MLXSW_SP_PORTS_PER_CLUSTER_MAX 4
#define MLXSW_SP_PORT_BASE_SPEED_25G 25000 /* Mb/s */ #define MLXSW_SP_PORT_BASE_SPEED_25G 25000 /* Mb/s */
#define MLXSW_SP_PORT_BASE_SPEED_50G 50000 /* Mb/s */ #define MLXSW_SP_PORT_BASE_SPEED_50G 50000 /* Mb/s */
...@@ -143,6 +141,12 @@ struct mlxsw_sp_port_type_speed_ops; ...@@ -143,6 +141,12 @@ struct mlxsw_sp_port_type_speed_ops;
struct mlxsw_sp_ptp_state; struct mlxsw_sp_ptp_state;
struct mlxsw_sp_ptp_ops; struct mlxsw_sp_ptp_ops;
struct mlxsw_sp_port_mapping {
u8 module;
u8 width;
u8 lane;
};
struct mlxsw_sp { struct mlxsw_sp {
struct mlxsw_sp_port **ports; struct mlxsw_sp_port **ports;
struct mlxsw_core *core; struct mlxsw_core *core;
...@@ -150,7 +154,7 @@ struct mlxsw_sp { ...@@ -150,7 +154,7 @@ struct mlxsw_sp {
unsigned char base_mac[ETH_ALEN]; unsigned char base_mac[ETH_ALEN];
const unsigned char *mac_mask; const unsigned char *mac_mask;
struct mlxsw_sp_upper *lags; struct mlxsw_sp_upper *lags;
int *port_to_module; struct mlxsw_sp_port_mapping **port_mapping;
struct mlxsw_sp_sb *sb; struct mlxsw_sp_sb *sb;
struct mlxsw_sp_bridge *bridge; struct mlxsw_sp_bridge *bridge;
struct mlxsw_sp_router *router; struct mlxsw_sp_router *router;
...@@ -259,11 +263,11 @@ struct mlxsw_sp_port { ...@@ -259,11 +263,11 @@ struct mlxsw_sp_port {
struct ieee_pfc *pfc; struct ieee_pfc *pfc;
enum mlxsw_reg_qpts_trust_state trust_state; enum mlxsw_reg_qpts_trust_state trust_state;
} dcb; } dcb;
struct { struct mlxsw_sp_port_mapping mapping; /* mapping is constant during the
u8 module; * mlxsw_sp_port lifetime, however
u8 width; * the same localport can have
u8 lane; * different mapping.
} mapping; */
/* TC handles */ /* TC handles */
struct list_head mall_tc_list; struct list_head mall_tc_list;
struct { struct {
...@@ -287,6 +291,7 @@ struct mlxsw_sp_port { ...@@ -287,6 +291,7 @@ struct mlxsw_sp_port {
u16 egr_types; u16 egr_types;
struct mlxsw_sp_ptp_port_stats stats; struct mlxsw_sp_ptp_port_stats stats;
} ptp; } ptp;
u8 split_base_local_port;
}; };
struct mlxsw_sp_port_type_speed_ops { struct mlxsw_sp_port_type_speed_ops {
......
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