From afdf3a8e1f925a467695e2ce6401ffab3cf32a9b Mon Sep 17 00:00:00 2001 From: Julius Bairaktaris Date: Sat, 22 Aug 2026 17:27:58 +0200 Subject: [PATCH] qualcommax: qca_ppe: offload ets and prio for the three bands The port's unicast scheduler is three strict bands, but nothing tells tc. Offload an ets or prio qdisc at the root when it describes those bands: three bands, served from band 0 down as tc numbers them, and a priomap that matches the band split ppe_qm_init programs or is the default one. sch_ets fills in a quantum for every band, so only quanta that differ between bands are refused. Neither qdisc programs the hardware; an accepted one reports the port's counters. Measured on IPQ8074: ets and prio with bands 3 and priomap 2 2 2 2 1 1 1 1 0 ... are offloaded, and two bands, unequal quanta and the inverted priomap are refused. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Julius Bairaktaris Link: https://github.com/openwrt/openwrt/pull/25700 Signed-off-by: Robert Marko --- .../drivers/net/ethernet/qualcomm/qca_ppe.h | 28 ++-- .../net/ethernet/qualcomm/qca_ppe_scheduler.c | 133 ++++++++++++++++++ 2 files changed, 151 insertions(+), 10 deletions(-) diff --git a/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe.h b/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe.h index 8bac6412291..e459662e03a 100644 --- a/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe.h +++ b/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe.h @@ -239,6 +239,18 @@ */ #define PPE_FLOW_SPREAD_QUEUES 4 +/* --- RSS hash (in the IPO block): unprogrammed, the 5-tuple hash is + * degenerate and every flow lands in one bucket. + */ +#define PPE_RSS_HASH_MASK (0x0b0000 + 0x4318) +#define PPE_RSS_HASH_SEED (0x0b0000 + 0x431c) +#define PPE_RSS_HASH_MIX(i) (0x0b0000 + 0x4320 + (i) * 0x4) +#define PPE_RSS_HASH_FIN(i) (0x0b0000 + 0x4350 + (i) * 0x4) +#define PPE_RSS_HASH_MASK_IPV4 (0x0b0000 + 0x4380) +#define PPE_RSS_HASH_SEED_IPV4 (0x0b0000 + 0x4384) +#define PPE_RSS_HASH_MIX_IPV4(i) (0x0b0000 + 0x4390 + (i) * 0x4) +#define PPE_RSS_HASH_FIN_IPV4(i) (0x0b0000 + 0x43b0 + (i) * 0x4) + /* --- L2 (base 0x060000) --- */ #define PPE_L2_BASE 0x060000 @@ -502,16 +514,6 @@ #define PPE_QM_UCAST_HASH_MAP(i) (PPE_QM_BASE + 0x30000 + (i) * 0x10) #define PPE_QM_HASH_CLASS GENMASK(3, 0) -/* The 5-tuple RSS hash in the IPO block. */ -#define PPE_RSS_HASH_MASK (0x0b0000 + 0x4318) -#define PPE_RSS_HASH_SEED (0x0b0000 + 0x431c) -#define PPE_RSS_HASH_MIX(i) (0x0b0000 + 0x4320 + (i) * 0x4) -#define PPE_RSS_HASH_FIN(i) (0x0b0000 + 0x4350 + (i) * 0x4) -#define PPE_RSS_HASH_MASK_IPV4 (0x0b0000 + 0x4380) -#define PPE_RSS_HASH_SEED_IPV4 (0x0b0000 + 0x4384) -#define PPE_RSS_HASH_MIX_IPV4(i) (0x0b0000 + 0x4390 + (i) * 0x4) -#define PPE_RSS_HASH_FIN_IPV4(i) (0x0b0000 + 0x43b0 + (i) * 0x4) - #define PPE_QM_UCAST_PRI_MAP(i) (PPE_QM_BASE + 0x42000 + (i) * 0x10) #define PPE_QM_PRI_CLASS GENMASK(3, 0) @@ -745,6 +747,8 @@ static inline struct qca_ppe_priv *ds_to_priv(struct dsa_switch *ds) u64 ppe_mib_read(struct qca_ppe_priv *priv, int port, unsigned int off); struct tc_tbf_qopt_offload; +struct tc_ets_qopt_offload; +struct tc_prio_qopt_offload; struct tc_query_caps_base; struct tc_mqprio_qopt_offload; @@ -762,6 +766,10 @@ int qca_ppe_port_set_apptrust(struct dsa_switch *ds, int port, const u8 *sel, int nsel); int qca_ppe_setup_tc_tbf(struct qca_ppe_priv *priv, int port, struct tc_tbf_qopt_offload *qopt); +int qca_ppe_setup_tc_ets(struct qca_ppe_priv *priv, int port, + struct tc_ets_qopt_offload *qopt); +int qca_ppe_setup_tc_prio(struct qca_ppe_priv *priv, int port, + struct tc_prio_qopt_offload *qopt); int qca_ppe_tc_query_caps(struct tc_query_caps_base *base); int qca_ppe_setup_tc_mqprio(struct qca_ppe_priv *priv, int port, struct tc_mqprio_qopt_offload *qopt); diff --git a/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe_scheduler.c b/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe_scheduler.c index 73c15609a2f..eb5faa0c089 100644 --- a/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe_scheduler.c +++ b/target/linux/qualcommax/files/drivers/net/ethernet/qualcomm/qca_ppe_scheduler.c @@ -1647,6 +1647,135 @@ int qca_ppe_tc_query_caps(struct tc_query_caps_base *base) return 0; } +/* The port's unicast scheduler is three strict bands built at probe and not + * reconfigurable: priorities eight and up at the top, the classified + * priorities under them, best-effort below both, and the queues inside a band + * a hash spread served round robin rather than a ladder of their own. ETS and + * prio are how that shape is expressed to tc, which numbers bands from the + * top, so the top band is band 0. A shape this hardware cannot give is refused + * rather than quietly flattened into one it can; nothing is programmed either + * way, an accepted qdisc only gains the port's counters. + */ +#define PPE_QOS_BANDS 3 + +static int ppe_qos_bands_set(struct qca_ppe_priv *priv, int port, u32 handle, + unsigned int bands, const u8 *priomap) +{ + struct ppe_port_shaper *sh = &priv->shaper[port]; + unsigned int i; + + if (bands != PPE_QOS_BANDS) { + dev_err(priv->ds.dev, + "port %d: %u bands, this scheduler is fixed at %d\n", + port, bands, PPE_QOS_BANDS); + return -EINVAL; + } + + /* The priomap the qdisc fills in when the user gave none puts every + * priority on the last band, which is an absence of an opinion rather + * than a different one. + */ + for (i = 0; i < TC_PRIO_MAX + 1; i++) + if (priomap[i] != bands - 1) + break; + + if (i < TC_PRIO_MAX + 1) { + for (i = 0; i < TC_PRIO_MAX + 1; i++) { + /* The band split ppe_qm_init gives a user port. */ + u8 band = PPE_QOS_BANDS - 1 - + min_t(unsigned int, + i / PPE_FLOW_SPREAD_QUEUES, + PPE_QOS_BANDS - 1); + + if (priomap[i] == band) + continue; + + dev_err(priv->ds.dev, + "port %d: priority %u is mapped to band %u; this scheduler is fixed and gives it band %u\n", + port, i, priomap[i], band); + return -EOPNOTSUPP; + } + } + + sh->bands_handle = handle; + ppe_port_tx_counters(priv, port, &sh->base_bytes, &sh->base_pkts, + &sh->base_drops); + + return 0; +} + +int qca_ppe_setup_tc_ets(struct qca_ppe_priv *priv, int port, + struct tc_ets_qopt_offload *qopt) +{ + struct ppe_port_shaper *sh = &priv->shaper[port]; + unsigned int i; + + if (qopt->parent != TC_H_ROOT) + return -EOPNOTSUPP; + + switch (qopt->command) { + case TC_ETS_REPLACE: + /* sch_ets gives every band a quantum whether the user asked + * for one or not, so a non-zero quantum is not a request. Bands + * that differ are: refuse those, because the bands are strict + * and the only round robin under them is between a band's hash + * buckets, which no priority selects. + */ + for (i = 1; i < qopt->replace_params.bands; i++) { + if (qopt->replace_params.quanta[i] == + qopt->replace_params.quanta[0]) + continue; + + dev_err(priv->ds.dev, + "port %d: band %u asks for a weight of its own; these bands are strict\n", + port, i); + return -EOPNOTSUPP; + } + + return ppe_qos_bands_set(priv, port, qopt->handle, + qopt->replace_params.bands, + qopt->replace_params.priomap); + case TC_ETS_DESTROY: + if (qopt->handle == sh->bands_handle) + sh->bands_handle = 0; + return 0; + case TC_ETS_STATS: + if (!sh->bands_handle || qopt->handle != sh->bands_handle) + return -EOPNOTSUPP; + ppe_port_shaper_stats(priv, port, &qopt->stats); + return 0; + default: + return -EOPNOTSUPP; + } +} + +int qca_ppe_setup_tc_prio(struct qca_ppe_priv *priv, int port, + struct tc_prio_qopt_offload *qopt) +{ + struct ppe_port_shaper *sh = &priv->shaper[port]; + + if (qopt->parent != TC_H_ROOT) + return -EOPNOTSUPP; + + switch (qopt->command) { + case TC_PRIO_REPLACE: + return ppe_qos_bands_set(priv, port, qopt->handle, + qopt->replace_params.bands, + qopt->replace_params.priomap); + case TC_PRIO_DESTROY: + if (qopt->handle == sh->bands_handle) + sh->bands_handle = 0; + return 0; + case TC_PRIO_STATS: + if (!sh->bands_handle || qopt->handle != sh->bands_handle) + return -EOPNOTSUPP; + ppe_port_shaper_stats(priv, port, &qopt->stats); + return 0; + default: + return -EOPNOTSUPP; + } +} + /* The hardware has one shaper per port and nowhere to hang a class off it, so * only a root tbf is a rate this switch can keep. The qdisc's overhead, mpu and * linklayer are not carried into the hardware, which meters the frame it puts @@ -1704,6 +1833,10 @@ int qca_ppe_setup_tc(struct dsa_switch *ds, int port, enum tc_setup_type type, switch (type) { case TC_SETUP_QDISC_TBF: return qca_ppe_setup_tc_tbf(priv, port, type_data); + case TC_SETUP_QDISC_ETS: + return qca_ppe_setup_tc_ets(priv, port, type_data); + case TC_SETUP_QDISC_PRIO: + return qca_ppe_setup_tc_prio(priv, port, type_data); case TC_SETUP_QDISC_MQPRIO: return qca_ppe_setup_tc_mqprio(priv, port, type_data); case TC_QUERY_CAPS: