[PATCH can-next v2] can: m_can: switch to rx-offload implementation

From: Marc Kleine-Budde

Date: Fri Jul 10 2026 - 08:09:46 EST


The current m_can driver uses NAPI for mmio devices to handle RX'ed CAN
frames, the RX IRQ is disabled and a NAPI poll is scheduled. Then in
m_can_poll() the RX'ed CAN frames are read from the device.

The driver already uses rx-offload for SPI devices like the tcan4x5x,
indicated by struct m_can_classdev::is_peripheral being set.

This approach has 2 drawbacks:

- Under high system load it might take too long from the initial RX IRQ to
the NAPI poll function to run. This causes RX buffer overflows.
- The driver contains several checks if it handles a peripheral or a memory
mapped device, that makes maintenance harder.

Convert the driver to unconditionally call m_can_rx_handler() from the IRQ
handler (m_can_interrupt_handler()), which reads the RX'ed CAN frames from
the hardware and adds it to a list sorted by RX timestamp. This list of
RX'ed SKBs is then passed to the networking stack in a later NAPI context.

Remove all manual napi handling from the driver and keep the
can_rx_offload_*().

Signed-off-by: Marc Kleine-Budde <mkl@xxxxxxxxxxxxxx>
---
Changes in v2:
- m_can_receive_skb(): remove double accounting of RX packets (found by sashiko)
- remove obsolete struct m_can_classdev::napi (found by sashiko)
- m_can_do_rx_poll(): remove quota, read all RX'ed messages (found by sashiko)
- Link to v1: https://patch.msgid.link/20260709-m_can-rx-offload-v1-1-af3efa8e4272@xxxxxxxxxxxxxx

To: Markus Schneider-Pargmann <msp@xxxxxxxxxxxx>
To: Marc Kleine-Budde <mkl@xxxxxxxxxxxxxx>
To: Vincent Mailhol <mailhol@xxxxxxxxxx>
Cc: linux-can@xxxxxxxxxxxxxxx
Cc: linux-kernel@xxxxxxxxxxxxxxx
---
drivers/net/can/m_can/m_can.c | 151 +++++++++++-------------------------------
drivers/net/can/m_can/m_can.h | 2 -
2 files changed, 38 insertions(+), 115 deletions(-)

diff --git a/drivers/net/can/m_can/m_can.c b/drivers/net/can/m_can/m_can.c
index eb856547ae7d..866c4b501dad 100644
--- a/drivers/net/can/m_can/m_can.c
+++ b/drivers/net/can/m_can/m_can.c
@@ -530,26 +530,17 @@ static void m_can_clean(struct net_device *net)
spin_unlock_irqrestore(&cdev->tx_handling_spinlock, irqflags);
}

-/* For peripherals, pass skb to rx-offload, which will push skb from
- * napi. For non-peripherals, RX is done in napi already, so push
- * directly. timestamp is used to ensure good skb ordering in
- * rx-offload and is ignored for non-peripherals.
- */
static void m_can_receive_skb(struct m_can_classdev *cdev,
struct sk_buff *skb,
u32 timestamp)
{
- if (cdev->is_peripheral) {
- struct net_device_stats *stats = &cdev->net->stats;
- int err;
+ struct net_device_stats *stats = &cdev->net->stats;
+ int err;

- err = can_rx_offload_queue_timestamp(&cdev->offload, skb,
- timestamp);
- if (err)
- stats->rx_fifo_errors++;
- } else {
- netif_receive_skb(skb);
- }
+ err = can_rx_offload_queue_timestamp(&cdev->offload, skb,
+ timestamp);
+ if (err)
+ stats->rx_fifo_errors++;
}

static int m_can_read_fifo(struct net_device *dev, u32 fgi)
@@ -600,10 +591,7 @@ static int m_can_read_fifo(struct net_device *dev, u32 fgi)
cf->data, DIV_ROUND_UP(cf->len, 4));
if (err)
goto out_free_skb;
-
- stats->rx_bytes += cf->len;
}
- stats->rx_packets++;

timestamp = FIELD_GET(RX_BUF_RXTS_MASK, fifo_header.dlc) << 16;

@@ -618,10 +606,9 @@ static int m_can_read_fifo(struct net_device *dev, u32 fgi)
return err;
}

-static int m_can_do_rx_poll(struct net_device *dev, int quota)
+static int m_can_do_rx_poll(struct net_device *dev)
{
struct m_can_classdev *cdev = netdev_priv(dev);
- u32 pkts = 0;
u32 rxfs;
u32 rx_count;
u32 fgi;
@@ -638,13 +625,11 @@ static int m_can_do_rx_poll(struct net_device *dev, int quota)
rx_count = FIELD_GET(RXFS_FFL_MASK, rxfs);
fgi = FIELD_GET(RXFS_FGI_MASK, rxfs);

- for (i = 0; i < rx_count && quota > 0; ++i) {
+ for (i = 0; i < rx_count; ++i) {
err = m_can_read_fifo(dev, fgi);
if (err)
break;

- quota--;
- pkts++;
ack_fgi = fgi;
fgi = (++fgi >= cdev->mcfg[MRAM_RXF0].num ? 0 : fgi);
}
@@ -652,10 +637,7 @@ static int m_can_do_rx_poll(struct net_device *dev, int quota)
if (ack_fgi != -1)
m_can_write(cdev, M_CAN_RXF0A, ack_fgi);

- if (err)
- return err;
-
- return pkts;
+ return err;
}

static int m_can_handle_lost_msg(struct net_device *dev)
@@ -678,8 +660,7 @@ static int m_can_handle_lost_msg(struct net_device *dev)
frame->can_id |= CAN_ERR_CRTL;
frame->data[1] = CAN_ERR_CRTL_RX_OVERFLOW;

- if (cdev->is_peripheral)
- timestamp = m_can_get_timestamp(cdev);
+ timestamp = m_can_get_timestamp(cdev);

m_can_receive_skb(cdev, skb, timestamp);

@@ -750,8 +731,7 @@ static int m_can_handle_lec_err(struct net_device *dev,
if (unlikely(!skb))
return 0;

- if (cdev->is_peripheral)
- timestamp = m_can_get_timestamp(cdev);
+ timestamp = m_can_get_timestamp(cdev);

m_can_receive_skb(cdev, skb, timestamp);

@@ -883,8 +863,7 @@ static int m_can_handle_state_change(struct net_device *dev,
break;
}

- if (cdev->is_peripheral)
- timestamp = m_can_get_timestamp(cdev);
+ timestamp = m_can_get_timestamp(cdev);

m_can_receive_skb(cdev, skb, timestamp);

@@ -973,8 +952,7 @@ static int m_can_handle_protocol_error(struct net_device *dev, u32 irqstatus)
return 0;
}

- if (cdev->is_peripheral)
- timestamp = m_can_get_timestamp(cdev);
+ timestamp = m_can_get_timestamp(cdev);

m_can_receive_skb(cdev, skb, timestamp);

@@ -1055,7 +1033,7 @@ static int m_can_rx_handler(struct net_device *dev, int quota, u32 irqstatus)
m_can_read(cdev, M_CAN_PSR));

if (irqstatus & IR_RF0N) {
- rx_work_or_err = m_can_do_rx_poll(dev, (quota - work_done));
+ rx_work_or_err = m_can_do_rx_poll(dev);
if (rx_work_or_err < 0)
return rx_work_or_err;

@@ -1065,32 +1043,6 @@ static int m_can_rx_handler(struct net_device *dev, int quota, u32 irqstatus)
return work_done;
}

-static int m_can_poll(struct napi_struct *napi, int quota)
-{
- struct net_device *dev = napi->dev;
- struct m_can_classdev *cdev = netdev_priv(dev);
- int work_done;
- u32 irqstatus;
-
- irqstatus = cdev->irqstatus | m_can_read(cdev, M_CAN_IR);
-
- work_done = m_can_rx_handler(dev, quota, irqstatus);
-
- /* Don't re-enable interrupts if the driver had a fatal error
- * (e.g., FIFO read failure).
- */
- if (work_done >= 0 && work_done < quota) {
- napi_complete_done(napi, work_done);
- m_can_enable_all_interrupts(cdev);
- }
-
- return work_done;
-}
-
-/* Echo tx skb and update net stats. Peripherals use rx-offload for
- * echo. timestamp is used for peripherals to ensure correct ordering
- * by rx-offload, and is ignored for non-peripherals.
- */
static unsigned int m_can_tx_update_stats(struct m_can_classdev *cdev,
unsigned int msg_mark, u32 timestamp)
{
@@ -1098,14 +1050,11 @@ static unsigned int m_can_tx_update_stats(struct m_can_classdev *cdev,
struct net_device_stats *stats = &dev->stats;
unsigned int frame_len;

- if (cdev->is_peripheral)
- stats->tx_bytes +=
- can_rx_offload_get_echo_skb_queue_timestamp(&cdev->offload,
- msg_mark,
- timestamp,
- &frame_len);
- else
- stats->tx_bytes += can_get_echo_skb(dev, msg_mark, &frame_len);
+ stats->tx_bytes +=
+ can_rx_offload_get_echo_skb_queue_timestamp(&cdev->offload,
+ msg_mark,
+ timestamp,
+ &frame_len);

stats->tx_packets++;

@@ -1265,21 +1214,10 @@ static int m_can_interrupt_handler(struct m_can_classdev *cdev)
if (cdev->ops->clear_interrupts)
cdev->ops->clear_interrupts(cdev);

- /* schedule NAPI in case of
- * - rx IRQ
- * - state change IRQ
- * - bus error IRQ and bus error reporting
- */
if (ir & (IR_RF0N | IR_RF0W | IR_ERR_ALL_30X)) {
- cdev->irqstatus = ir;
- if (!cdev->is_peripheral) {
- m_can_disable_all_interrupts(cdev);
- napi_schedule(&cdev->napi);
- } else {
- ret = m_can_rx_handler(dev, NAPI_POLL_WEIGHT, ir);
- if (ret < 0)
- return ret;
- }
+ ret = m_can_rx_handler(dev, NAPI_POLL_WEIGHT, ir);
+ if (ret < 0)
+ return ret;
}

if (cdev->version == 30) {
@@ -1288,8 +1226,7 @@ static int m_can_interrupt_handler(struct m_can_classdev *cdev)
u32 timestamp = 0;
unsigned int frame_len;

- if (cdev->is_peripheral)
- timestamp = m_can_get_timestamp(cdev);
+ timestamp = m_can_get_timestamp(cdev);
frame_len = m_can_tx_update_stats(cdev, 0, timestamp);
m_can_finish_tx(cdev, 1, frame_len);
}
@@ -1304,6 +1241,8 @@ static int m_can_interrupt_handler(struct m_can_classdev *cdev)

if (cdev->is_peripheral)
can_rx_offload_threaded_irq_finish(&cdev->offload);
+ else
+ can_rx_offload_irq_finish(&cdev->offload);

return IRQ_HANDLED;
}
@@ -1752,9 +1691,6 @@ static int m_can_dev_setup(struct m_can_classdev *cdev)
if (err)
return err;

- if (!cdev->is_peripheral)
- netif_napi_add(dev, &cdev->napi, m_can_poll);
-
/* Shared properties of all M_CAN versions */
cdev->version = m_can_version;
cdev->can.do_set_mode = m_can_set_mode;
@@ -1846,11 +1782,10 @@ static int m_can_close(struct net_device *dev)
if (cdev->is_peripheral) {
destroy_workqueue(cdev->tx_wq);
cdev->tx_wq = NULL;
- can_rx_offload_disable(&cdev->offload);
- } else {
- napi_disable(&cdev->napi);
}

+ can_rx_offload_disable(&cdev->offload);
+
close_candev(dev);

reset_control_assert(cdev->rst);
@@ -2069,8 +2004,8 @@ static enum hrtimer_restart m_can_polling_timer(struct hrtimer *timer)

ret = m_can_interrupt_handler(cdev);

- /* On error or if napi is scheduled to read, stop the timer */
- if (ret < 0 || napi_is_scheduled(&cdev->napi))
+ /* On error stop the timer */
+ if (ret < 0)
return HRTIMER_NORESTART;

hrtimer_forward_now(timer, ms_to_ktime(HRTIMER_POLL_INTERVAL_MS));
@@ -2102,10 +2037,7 @@ static int m_can_open(struct net_device *dev)
goto out_reset_control_assert;
}

- if (cdev->is_peripheral)
- can_rx_offload_enable(&cdev->offload);
- else
- napi_enable(&cdev->napi);
+ can_rx_offload_enable(&cdev->offload);

/* register interrupt handler */
if (cdev->is_peripheral) {
@@ -2144,16 +2076,13 @@ static int m_can_open(struct net_device *dev)
return 0;

exit_start_fail:
- if (cdev->is_peripheral || dev->irq)
+ if (dev->irq)
free_irq(dev->irq, dev);
exit_irq_fail:
if (cdev->is_peripheral)
destroy_workqueue(cdev->tx_wq);
out_wq_fail:
- if (cdev->is_peripheral)
- can_rx_offload_disable(&cdev->offload);
- else
- napi_disable(&cdev->napi);
+ can_rx_offload_disable(&cdev->offload);
close_candev(dev);
out_reset_control_assert:
reset_control_assert(cdev->rst);
@@ -2533,12 +2462,10 @@ int m_can_class_register(struct m_can_classdev *cdev)
if (ret)
goto clk_disable;

- if (cdev->is_peripheral) {
- ret = can_rx_offload_add_manual(cdev->net, &cdev->offload,
- NAPI_POLL_WEIGHT);
- if (ret)
- goto out_reset_control_assert;
- }
+ ret = can_rx_offload_add_manual(cdev->net, &cdev->offload,
+ NAPI_POLL_WEIGHT);
+ if (ret)
+ goto out_reset_control_assert;

if (!cdev->net->irq) {
netdev_dbg(cdev->net, "Polling enabled, initialize hrtimer");
@@ -2575,8 +2502,7 @@ int m_can_class_register(struct m_can_classdev *cdev)
return 0;

rx_offload_del:
- if (cdev->is_peripheral)
- can_rx_offload_del(&cdev->offload);
+ can_rx_offload_del(&cdev->offload);
out_reset_control_assert:
reset_control_assert(cdev->rst);
clk_disable:
@@ -2589,8 +2515,7 @@ EXPORT_SYMBOL_GPL(m_can_class_register);
void m_can_class_unregister(struct m_can_classdev *cdev)
{
unregister_candev(cdev->net);
- if (cdev->is_peripheral)
- can_rx_offload_del(&cdev->offload);
+ can_rx_offload_del(&cdev->offload);
}
EXPORT_SYMBOL_GPL(m_can_class_unregister);

diff --git a/drivers/net/can/m_can/m_can.h b/drivers/net/can/m_can/m_can.h
index 4743342b2fba..9223a77b55d7 100644
--- a/drivers/net/can/m_can/m_can.h
+++ b/drivers/net/can/m_can/m_can.h
@@ -81,7 +81,6 @@ struct m_can_tx_op {
struct m_can_classdev {
struct can_priv can;
struct can_rx_offload offload;
- struct napi_struct napi;
struct net_device *net;
struct device *dev;
struct clk *hclk;
@@ -96,7 +95,6 @@ struct m_can_classdev {
const struct m_can_ops *ops;

int version;
- u32 irqstatus;

int pm_clock_support;
int pm_wake_source;

---
base-commit: 08030ddb87b4c6c6a2c03c82731b5e188f02f5b9
change-id: 20260709-m_can-rx-offload-a228500d9f3a

Best regards,
--
Marc Kleine-Budde <mkl@xxxxxxxxxxxxxx>