Linux CAN drivers development
 help / color / mirror / Atom feed
* [PATCH v9 0/2] Add Loongson CAN-FD controller driver
@ 2026-11-09  8:42 Binbin Zhou
  2026-11-09  8:42 ` [PATCH v9 1/2] can: " Binbin Zhou
  2026-11-09  8:42 ` [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support Binbin Zhou
  0 siblings, 2 replies; 7+ messages in thread
From: Binbin Zhou @ 2026-11-09  8:42 UTC (permalink / raw)
  To: Binbin Zhou, Huacai Chen, Marc Kleine-Budde, Vincent Mailhol,
	Bingxiong Li
  Cc: Huacai Chen, Xuerui Wang, loongarch, linux-can, jeffbai,
	Binbin Zhou

Hi all:

This patchset adds support for the CAN-FD controller found on Loongson
CPUs.

Patch 1 introduces the basic CAN-FD controller driver with support
for classic CAN and CAN FD, including bit timing, error handling,
NAPI-based RX, and multiple TX buffers.

Patch 2 adds optional DMA support for RX path using the Loongson APB
CMC DMA engine, which significantly reduces CPU load under high
receive throughput.

The driver has been tested on Loongson-2K3000 platforms with various
CAN/CAN FD traffic patterns.

Finally, I'd like to thank Bingxiong, the original author of this driver,
for his efforts in working on the patch.

Thanks.
Binbin

=========
V9:
Patch (1/2):
 - Add hardware timestamping support;
 - Refactor RX into descriptor parsing/validation/delivery;
 - Improve IRQ handling: add DOI overrun handlinga;
 - Improve TX path: add free TX buffer helper, support CC_LEN8_DLC;
 - Split error reporting into state-change/arb-loss/bus-error helpers;
 - Cancel restart_work before free_irq();
Patch (2/2):
 - Enlarge RX DMA ring to 32K and use 4K cyclic DMA periods.
 - Split DMA init from start/stop; start DMA before chip_start(),
   stop after chip_stop(), and restart DMA on CAN_MODE_START.
 - Add DMA RX empty/overrun/bad-header recovery, partial-header -EAGAIN
   retry with bounded cpu_relax(), and pointer resync.
 - Rename RX callbacks to read_rx_word/rx_is_empty/process_rx;
 - Release DMA on probe/register error and in remove().

Testing & Validation:
----------------------
To validate the robustness and data integrity of this driver patch, extensive stress
tests were conducted across two different speed configurations under high bus load. 

To prove both system stability and data correctness, the test suite introduced
strict data integrity checks at the payload level:

1. Traffic Generation & Load Control:
   - Traffic consists of both periodic business PDUs (20ms, 50ms, 100ms, 200ms)
     and 64-byte fill frames.
   - An accumulator-based algorithm is used to precisely maintain an 80% total bus
     load on both ends without bursting the driver queues.

2. Data Integrity Verification:
   - Each frame payload embeds a 32-bit sequence number (SEQ32), a deterministic data
     fingerprint, and a CRC32 checksum.
   - The receiving ends strictly verify every received frame (including fill frames)
     against these metrics to detect any bit flips, payload corruption, or frame
     duplication.

3. Test Case A: 500 kbps / 2 Mbps (Industry Baseline)
   - Duration: ~66 hours.
   - Throughput: Over 20 million business frames and 160 million fill frames.
   - Results: 0 CRC errors, 0 fingerprint mismatches, and 0 duplicated frames. The overall
     frame loss rate was 0.00%. The driver handled transient bus interference gracefully
     (2 "error-warn" events) without any Bus-Off.

4. Test Case B: 1 Mbps / 4 Mbps (High-Speed Stress)
   - Duration: ~45.5 hours.
   - Throughput: Over 13.9 million business frames and 227 million fill frames.
   - Data Integrity: 0 CRC errors and 0 fingerprint mismatches across all hundreds of millions
     of frames.
   - Driver Recovery: During this high-speed test, minor physical layer interference triggered
     the `ERROR-PASSIVE` state (TEC=128). However, the driver successfully recovered, and no
     Bus-Off was observed.

The complete test suite (C and Python) and raw test logs have been published for reproducibility
and can be found here:

zlg python link: https://gist.github.com/AaronDot/e33f48c6c89d64e6b3564d9125ac2b82
linux C link: https://gist.github.com/AaronDot/6b7bbbc07a378fc263ccbc9f4c42e23a
500k+2M result link: https://gist.github.com/AaronDot/2ec489b1175d52f87bb9ba043168b80c
1M+4M result link: https://gist.github.com/AaronDot/a91fb52ffcec859b0eddc4f89b64eb3c

Link to V8:
https://lore.kernel.org/all/cover.1788945497.git.zhoubinbin@loongson.cn/

V8:
Patch (1/2):
 - Use local copy of can_bittiming in set_btr to avoid corrupting
   persistent structure;
 - Fix endianness and strict aliasing in TX/RX data paths;
 - Fixed spelling errors;
Patch (2/2):
 - Add overflow detection for RXDMA;
 - Simplified callback function names

Link to V7:
https://lore.kernel.org/all/cover.1785898181.git.zhoubinbin@loongson.cn/

V7:
Patch (1/2):
 - Fix can_bittiming_const->tseg1_max;
 - Fix TX buffer id in loongson_canfd_start_xmit();
 - Add rwcnt checking in loongson_canfd_rx()
 - Add netdev stats spinlock;
 - Add net_ratelimit() to control log output in
   loongson_canfd_err_interrupt);
 - Rewrite `loongson_canfd_rx_napi()` to clearly distinguish between the
   two scenarios: "no more data to process" and "quota exhausted".

Patch (2/2):
 - Put dmaengine_terminate_sync() before loongson_canfd_rxdma_free();

Link to V6:
https://lore.kernel.org/all/cover.1784597141.git.zhoubinbin@loongson.cn/

V6:
- Rebase on linux-can/master;
Patch (1/2):
 - Drop CAN_CTRLMODE_3_SAMPLES ctrlmode;
 - Replace kfree_skb() with dev_kfree_skb_any();
 - Drop REG_CMD_ERCRST operation;
 - Rewrite `loongson_canfd_tx_interrupt()` to prevent duplicate handling
   of TX completion';
Patch (2/2):
 - Added a fallback to true when dmaengine_tx_status() returns an error;
 - Replaced all direct regmap_read() from LOONGSON_CANFD_RX_DATA with
   the priv->get_rx_data() callback;
 - Release the DMA channel and clear priv->rx_ch on dma_alloc_coherent()
   failure;

Link to V5:
https://lore.kernel.org/all/cover.1783670733.git.zhoubinbin@loongson.cn/

V5:
Patch (1/2):
 - Fix tseg1_max const value;
 - Empty FIFO when SKB allocate failed.
 - Use REG_FRAME_META1_RWCNT as the playload read count value;
 - Only clean processed interrupt flags in
   loongson_canfd_err_interrupt();
 - Refact loongson_canfd_tx_interrupt();
 - Add LOONGSON_CANFD_TRV_DLY into loongson_canfd_volatile_reg();
Patch (2/2):
 - Fix dma_request_chan() error path;
 - Fix build error;
 - Fix loongson_canfd_rxdma_init() error path;

Link to V4:
https://lore.kernel.org/all/cover.1783391759.git.zhoubinbin@loongson.cn/

V4:
- Fix some issues from sashiko.
Patch (1/2):
 - Update commit message;
 - Use common APIs to calculate DLC;
 - Correct CAN_RTR_FLAG judgement;
 - Add skb check for CAN_STATE_ERROR_ACTIVE case;
 - Use regmap_write() rather than regmap_update_bits() to
   disable all interrupts;
 - Add napi_disable() in error paths;
 - Add `clock-frequency` return value check;
 - More comments for macros.

Link to V3:
https://lore.kernel.org/all/cover.1782787997.git.zhoubinbin@loongson.cn/

V3:
Patch (1/2):
 - Replace the switch-case statement with a calculated mask in
   `loongson_canfd_get_txbuf_sts()`;
 - Change `loongson_canfd_reset()` return type to `void`;
 - Drop old code in `loongson_canfd_set_btr()`;
 - Rename `loongson_canfd_set_mode()` to `loongson_canfd_set_conf_mode()`;
 - Drop `cf->__res*` usage;
 - Drop unnecessary macro definitions;
 - Add `netif_napi_del()` in probe error route path;
 - Replace ternary operator with `if-else`;
 - Drop stats->rx_packets and stats->rx_bytes count in CAN error frames;
 - Add ethtool_ops;
 - Add CAN_CTRLMODE_CC_LEN8_DLC;

Patch (2/2):
 - Fix W=2 warning;
 - Rename get_rx_frc() -> get_rxbuf_empty();
 - Put `loongson_canfd_rxdma_remove()` before free_candev();
 - Adjust the RXDMA probe process.

Link to V2:
https://lore.kernel.org/all/cover.1780908445.git.zhoubinbin@loongson.cn/

V2:
Patch (1/2):
 - Put all code into one file;
 - Add COMPILE_TEST Kconfig option;
 - Rewrite Kconfig description;
 - Use `regmap_test_bits()` to simplify bit field checks;
 - Drop odd FIELD_GET() usage;
 - Don't use FIELD_GET() for a single bit;
 - Use an if/else instead of the ternary operator;
 - Use the CAN TDC framework to get the SSP value;
 - Use guard(spinlock)/scoped_guard(spinlock);
 - Use netdev_debug() to be less verboss;
 - Check for memory allocation failure;
 - Add CAN_ERR_CNT flag;
 - Drop unused REG_DATA_xx_yy_W_DATA_yy definition;
 - Add more function comment;

Link to V1:
https://lore.kernel.org/all/cover.1777273055.git.zhoubinbin@loongson.cn/

Binbin Zhou (2):
  can: Add Loongson CAN-FD controller driver
  can: loongson_canfd: Add RXDMA support

 MAINTAINERS                      |    7 +
 drivers/net/can/Kconfig          |   11 +
 drivers/net/can/Makefile         |    1 +
 drivers/net/can/loongson_canfd.c | 3075 ++++++++++++++++++++++++++++++
 4 files changed, 3094 insertions(+)
 create mode 100644 drivers/net/can/loongson_canfd.c


base-commit: 647b81ddefc6364290e67bf33c89d54971f8995e
-- 
2.52.0


^ permalink raw reply	[flat|nested] 7+ messages in thread

* [PATCH v9 1/2] can: Add Loongson CAN-FD controller driver
  2026-11-09  8:42 [PATCH v9 0/2] Add Loongson CAN-FD controller driver Binbin Zhou
@ 2026-11-09  8:42 ` Binbin Zhou
  2026-11-09  9:31   ` sashiko-bot
  2026-11-10 22:20   ` netdev-bot+sashiko
  2026-11-09  8:42 ` [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support Binbin Zhou
  1 sibling, 2 replies; 7+ messages in thread
From: Binbin Zhou @ 2026-11-09  8:42 UTC (permalink / raw)
  To: Binbin Zhou, Huacai Chen, Marc Kleine-Budde, Vincent Mailhol,
	Bingxiong Li
  Cc: Huacai Chen, Xuerui Wang, loongarch, linux-can, jeffbai,
	Binbin Zhou

Add a driver for the CAN-FD controller integrated in the Loongson-2K
series SoCs.

The controller supports CAN 2.0B and CAN FD (ISO and non-ISO), and
provides an 80-byte RX FIFO, eight independent TX buffers, a flexible
RX buffer mode and a 16-bit free-running timestamp counter. The driver
exposes the counter as hardware RX/TX timestamps through the CAN
timestamping infrastructure.

Supported features include NAPI-based RX, TX echo, error reporting
(warning, passive, bus-off), loopback, listen-only, one-shot, FD and
non-ISO FD modes, and automatic TDC for the FD data phase.

Co-developed-by: Bingxiong Li <libingxiong@loongson.cn>
Signed-off-by: Bingxiong Li <libingxiong@loongson.cn>
Signed-off-by: Binbin Zhou <zhoubinbin@loongson.cn>
---
 MAINTAINERS                      |    7 +
 drivers/net/can/Kconfig          |   11 +
 drivers/net/can/Makefile         |    1 +
 drivers/net/can/loongson_canfd.c | 2414 ++++++++++++++++++++++++++++++
 4 files changed, 2433 insertions(+)
 create mode 100644 drivers/net/can/loongson_canfd.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 09713dc4c6a7..d5d07798ae6a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15646,6 +15646,13 @@ F:	Documentation/devicetree/bindings/sound/loongson,ls2k1000-i2s.yaml
 F:	sound/soc/loongson/loongson_*.c
 F:	sound/soc/loongson/loongson_*.h
 
+LOONGSON CAN FD DRIVER
+M:	Bingxiong Li <libingxiong@loongson.cn>
+M:	Binbin Zhou <zhoubinbin@loongson.cn>
+L:	linux-can@vger.kernel.org
+S:	Maintained
+F:	drivers/net/can/loongson_canfd.c
+
 LOONGSON GPIO DRIVER
 M:	Yinbo Zhu <zhuyinbo@loongson.cn>
 L:	linux-gpio@vger.kernel.org
diff --git a/drivers/net/can/Kconfig b/drivers/net/can/Kconfig
index a8fad6fe5302..95a0f898959c 100644
--- a/drivers/net/can/Kconfig
+++ b/drivers/net/can/Kconfig
@@ -186,6 +186,17 @@ config CAN_KVASER_PCIEFD
 	    Kvaser M.2 PCIe 4xCAN
 	    Kvaser PCIe 8xCAN
 
+config CAN_LOONGSON_CANFD
+	tristate "Loongson CAN-FD controller"
+	depends on HAS_IOMEM
+	select REGMAP_MMIO
+	help
+	  This is a canfd driver switch for the Loongson platform,
+	  integrated with the Loongson-2K series SoCs.
+
+	  To compile as a module, choose M here: the module will be
+	  called loongson_canfd.
+
 config CAN_SLCAN
 	tristate "Serial / USB serial CAN Adaptors (slcan)"
 	depends on TTY
diff --git a/drivers/net/can/Makefile b/drivers/net/can/Makefile
index 4010d17f8583..c4eae4595ce3 100644
--- a/drivers/net/can/Makefile
+++ b/drivers/net/can/Makefile
@@ -27,6 +27,7 @@ obj-$(CONFIG_CAN_GRCAN)		+= grcan.o
 obj-$(CONFIG_CAN_IFI_CANFD)	+= ifi_canfd/
 obj-$(CONFIG_CAN_JANZ_ICAN3)	+= janz-ican3.o
 obj-$(CONFIG_CAN_KVASER_PCIEFD)	+= kvaser_pciefd/
+obj-$(CONFIG_CAN_LOONGSON_CANFD)	+= loongson_canfd.o
 obj-$(CONFIG_CAN_MSCAN)		+= mscan/
 obj-$(CONFIG_CAN_M_CAN)		+= m_can/
 obj-$(CONFIG_CAN_PEAK_PCIEFD)	+= peak_canfd/
diff --git a/drivers/net/can/loongson_canfd.c b/drivers/net/can/loongson_canfd.c
new file mode 100644
index 000000000000..7a4d8df7815a
--- /dev/null
+++ b/drivers/net/can/loongson_canfd.c
@@ -0,0 +1,2414 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * LOONGSON CANFD controller driver
+ *
+ * Copyright (C) 2024-2026 Loongson Technology Corporation Limited
+ */
+
+#include <linux/acpi.h>
+#include <linux/bitfield.h>
+#include <linux/bits.h>
+#include <linux/can/dev.h>
+#include <linux/can/error.h>
+#include <linux/cleanup.h>
+#include <linux/clocksource.h>
+#include <linux/ethtool.h>
+#include <linux/interrupt.h>
+#include <linux/module.h>
+#include <linux/netdevice.h>
+#include <linux/net_tstamp.h>
+#include <linux/platform_device.h>
+#include <linux/regmap.h>
+#include <linux/skbuff.h>
+#include <linux/string.h>
+#include <linux/timecounter.h>
+#include <linux/timekeeping.h>
+#include <linux/types.h>
+#include <linux/unaligned.h>
+
+#define LOONGSON_CANFD_DEVICE_ID	0x0	/* CANFD controller ID Register */
+#define LOONGSON_CANFD_MODE		0x4	/* Mode Configuration Register */
+#define LOONGSON_CANFD_CONF		0x8	/* Configure Register */
+#define LOONGSON_CANFD_STAT		0xc	/* Status Register */
+#define LOONGSON_CANFD_CMD		0x10	/* Command Register */
+#define LOONGSON_CANFD_INT_STAT		0x14	/* Interrupt Status Register */
+#define LOONGSON_CANFD_INT_ENA		0x18	/* Interrupt Enable Register */
+#define LOONGSON_CANFD_INT_MASK		0x1c	/* Interrupt Mask Register */
+#define LOONGSON_CANFD_BTR_NORM		0x20	/* Normal Rate Configuration Register */
+#define LOONGSON_CANFD_BTR_FD		0x24	/* FD Data Rate Configuration Register */
+#define LOONGSON_CANFD_ERL		0x28	/* Error Threshold Configuration Register */
+#define LOONGSON_CANFD_FSTAT		0x2c	/* Error Status Register */
+#define LOONGSON_CANFD_ERC		0x30	/* Error Count Register */
+#define LOONGSON_CANFD_BRE		0x34	/* Rate Error Count Register */
+#define LOONGSON_CANFD_CTR_PRES		0x38	/* Error Count Debug Register */
+#define LOONGSON_CANFD_ERR_CAPT		0x3c	/* Error Capture Status Register */
+#define LOONGSON_CANFD_RETX_CNT		0x40	/* Retransmission Count Register */
+#define LOONGSON_CANFD_ALC		0x44	/* Lost Arbitration Capture Register */
+#define LOONGSON_CANFD_TRV_DLY		0x48	/* Transmission Delay Measurement Register */
+#define LOONGSON_CANFD_SSP_CFG		0x4c	/* Second Sampling Point Configuration Register */
+#define LOONGSON_CANFD_RX_FR_CNT	0x50	/* Receive Message Count Register */
+#define LOONGSON_CANFD_TX_FR_CNT	0x54	/* Transmit Message Count Register */
+#define LOONGSON_CANFD_DEBUG		0x58	/* Debug Register */
+#define LOONGSON_CANFD_TS		0x5c	/* Timestamp Register */
+#define LOONGSON_CANFD_TX_FRM_TST	0x60	/* Transmit Message Debug Register */
+#define LOONGSON_CANFD_FRC_DIV		0x64	/* Fractional Divider Ratio Register */
+#define LOONGSON_CANFD_FLT_A_MASK	0x68	/* Filter A Mask Register */
+#define LOONGSON_CANFD_FLT_A_VAL	0x6c	/* Filter A value Register */
+#define LOONGSON_CANFD_FLT_B_MASK	0x70	/* Filter B Mask Register */
+#define LOONGSON_CANFD_FLT_B_VAL	0x74	/* Filter B value Register */
+#define LOONGSON_CANFD_FLT_C_MASK	0x78	/* Filter C Mask Register */
+#define LOONGSON_CANFD_FLT_C_VAL	0x7c	/* Filter C value Register */
+#define LOONGSON_CANFD_FLT_R_LOW	0x80	/* Range Filter Low Threshold Register */
+#define LOONGSON_CANFD_FLT_R_HI		0x84	/* Range Filter High Threshold Register */
+#define LOONGSON_CANFD_FLT_CTRL		0x88	/* Filter Control Register */
+#define LOONGSON_CANFD_RX_MEM_INFO	0x8c	/* Receive Buffer Information Register */
+#define LOONGSON_CANFD_RX_PRT		0x90	/* Receive Buffer Pointer Register */
+#define LOONGSON_CANFD_RX_STAT		0x94	/* Receive Buffer Status Register */
+#define LOONGSON_CANFD_RX_DATA		0x98	/* Receive Data Register */
+#define LOONGSON_CANFD_TX_STAT		0x9c	/* Transmit Buffer Status Register */
+#define LOONGSON_CANFD_TX_CMD		0xa0	/* Transmit Command Register */
+#define LOONGSON_CANFD_TX_SEL		0xa4	/* Transmit Buffer Selection Register */
+#define LOONGSON_CANFD_TX_DATA_1	0xb0
+#define LOONGSON_CANFD_TX_DATA_18	0xf4
+
+/* Bitfields of CANFD controller ID register */
+#define REG_ID_MASK			GENMASK(15, 0)
+#define REG_ID_VER_MIN			GENMASK(23, 16)
+#define REG_ID_VER_MAJ			GENMASK(31, 24)
+
+/* Bitfields of Mode Configuration register */
+#define REG_MODE_RST			BIT(0)	/* Soft Reset */
+#define REG_MODE_BMM			BIT(1)	/* Bus Monitoring Mode */
+#define REG_MODE_STM			BIT(2)	/* Self-Test Mode */
+#define REG_MODE_AFM			BIT(3)	/* Receive Filter Mode */
+#define REG_MODE_FDE			BIT(4)	/* CANFD Enabled */
+#define REG_MODE_TTTM			BIT(5)	/* Timed Transmission Mode */
+#define REG_MODE_ROM			BIT(6)	/* Restricted Operation Mode */
+#define REG_MODE_ACF			BIT(7)	/* No ACK Mode*/
+#define REG_MODE_TSTM			BIT(8)	/* Test Mode */
+#define REG_MODE_RXBAM			BIT(9)	/* Receive Buffer Auto Mode */
+#define REG_MODE_ITSM			BIT(10)	/* Internal Timestamp Mode */
+#define REG_MODE_RTSOP			BIT(12)	/* Receive Buffer Timestamp Addition Mode */
+#define REG_MODE_BUFM			BIT(13)	/* Transmit Buffer Mode */
+
+/* Bitfields of Configure register */
+#define REG_CONF_RTRLE			BIT(0)	/* Retransmission Threshold Enable */
+#define REG_CONF_RTRTH			GENMASK(4, 1)	/* Retransmission Threshold */
+#define REG_CONF_ILBP			BIT(5)	/* Internal Loopback Mode */
+#define REG_CONF_ENA			BIT(6)	/* Controller Enable */
+#define REG_CONF_NISOFD			BIT(7)	/* NON-ISO CANFD Protocol Enable */
+#define REG_CONF_PEX			BIT(8)	/* Protocol Exception Handling */
+#define REG_CONF_FDRF			BIT(10)	/* Ignore Remote Frame Enable */
+
+/* Bitfields of Status register */
+#define REG_STAT_RXNE			BIT(0)	/* Receive Buffer Not Empty Status */
+#define REG_STAT_DOR			BIT(1)	/* Receive Data Overflow Status */
+#define REG_STAT_EFT			BIT(3)	/* Error Frame Flag */
+#define REG_STAT_RXS			BIT(4)	/* Receive Status Flag */
+#define REG_STAT_TXS			BIT(5)	/* Transmission Status Flag */
+#define REG_STAT_EWL			BIT(6)	/* Transmission Error Threshold Flag */
+#define REG_STAT_IDLE			BIT(7)	/* Bus Idle Flag */
+#define REG_STAT_PEXS			BIT(8)	/* Protocol Exception Status */
+#define REG_STAT_STCNT			BIT(16)	/* Transmission Count Support */
+
+/* Bitfields of Command register */
+#define REG_CMD_RXRPMV			BIT(1)	/* Receive Buffer Read-to-Move */
+#define REG_CMD_RRB			BIT(2)	/* Receive Buffer Release */
+#define REG_CMD_CDO			BIT(3)	/* Received Data Overflow Status Clear */
+#define REG_CMD_ERCRST			BIT(4)	/* Error Count Clear */
+#define REG_CMD_RXFCRST			BIT(5)	/* Receive Frame Count Clear */
+#define REG_CMD_TXFCRST			BIT(6)	/* Transmit Frame Count Clear */
+#define REG_CMD_CPEXS			BIT(7)	/* Exception Status Clear */
+
+/* Bitfields of Interrupt Status register */
+#define REG_INT_STAT_RXI		BIT(0)	/* Message Reception Interrupt */
+#define REG_INT_STAT_TXI		BIT(1)	/* Message Transmission Interrupt */
+#define REG_INT_STAT_EWLI		BIT(2)	/* Error Warning Threshold Interrupt */
+#define REG_INT_STAT_DOI		BIT(3)	/* Receive Data Overflow Interrupt */
+#define REG_INT_STAT_FCSI		BIT(4)	/* Fault State Change Interrupt */
+#define REG_INT_STAT_ALI		BIT(5)	/* Arbitration Loss Interrupt */
+#define REG_INT_STAT_BEI		BIT(6)	/* Bus Error Interrupt */
+#define REG_INT_STAT_RXFI		BIT(7)	/* Receive Buffer Full Interrupt */
+#define REG_INT_STAT_BSI		BIT(8)	/* Rate Switching Interrupt */
+#define REG_INT_STAT_RBNEI		BIT(9)	/* Receive Buffer Not Empty Interrupt */
+#define REG_INT_STAT_TXBHCI		BIT(10)	/* Transmit Buffer Hardware Command Interrupt */
+#define REG_INT_STAT_OFI		BIT(11)	/* Overload Frame Transmission Interrupt */
+#define REG_INT_STAT_DMADI		BIT(12)	/* Message DMA Transfer Complete Interrupt */
+
+#define REG_INT_STAT_ERRORI		(REG_INT_STAT_EWLI | REG_INT_STAT_FCSI |\
+					 REG_INT_STAT_ALI | REG_INT_STAT_BEI)
+
+/* Bitfields of Interrupt Enable register */
+#define REG_INT_ENA_SET			GENMASK(12, 0)
+#define REG_INT_ENA_CLR			GENMASK(28, 16)
+
+/* Bitfields of Interrupt Mask register */
+#define REG_INT_MASK_SET		GENMASK(12, 0)
+#define REG_INT_MASK_CLR		GENMASK(28, 16)
+
+/* Bitfields of Normal Rate Configuration register */
+#define REG_BTR_PROP			GENMASK(6, 0)
+#define REG_BTR_PH1			GENMASK(12, 7)
+#define REG_BTR_PH2			GENMASK(18, 13)
+#define REG_BTR_BRP			GENMASK(22, 19)
+#define REG_BTR_SJW			GENMASK(31, 27)
+
+/* Bitfields of FD Data Rate Configuration register */
+#define REG_BTR_FD_PROP			GENMASK(6, 0)
+#define REG_BTR_FD_PH1			GENMASK(12, 7)
+#define REG_BTR_FD_PH2			GENMASK(18, 13)
+#define REG_BTR_FD_BRP			GENMASK(26, 19)
+#define REG_BTR_FD_SJW			GENMASK(31, 27)
+
+/* Bitfields of Error Threshold Configuration register */
+#define REG_ERL_ERP			GENMASK(7, 0)
+#define REG_ERL_EW			GENMASK(23, 16)
+
+/* Bitfields of Error Status register */
+#define REG_FSTAT_ERA			BIT(0)
+#define REG_FSTAT_ERP			BIT(1)
+#define REG_FSTAT_BOF			BIT(2)
+
+#define REG_FSTAT_MASK			GENMASK(2, 0)
+
+/* Bitfields of Error Count register */
+#define REG_ERC_TEC			GENMASK(8, 0)
+#define REG_ERC_REC			GENMASK(24, 16)
+
+/* Bitfields of Rate Error Count register */
+#define REG_BRE_NORM			GENMASK(15, 0)
+#define REG_BRE_FD_DATA			GENMASK(31, 16)
+
+/* Bitfields of Error Count Debug register */
+#define REG_CTR_PRES_CTPV		GENMASK(8, 0)
+#define REG_CTR_PRES_PTX		BIT(9)
+#define REG_CTR_PRES_PRX		BIT(10)
+
+/* Bitfields of Error Capture Status register */
+#define REG_ERR_CAPT_POS		GENMASK(4, 0)
+#define REG_ERR_CAPT_TYPE		GENMASK(7, 5)
+
+/* Bitfields of Retransmission Count register */
+#define REG_RETX_CNT_VAL		GENMASK(3, 0)
+
+/* Bitfields of Lost Arbitration Capture register */
+#define REG_ALC_BIT_POS			GENMASK(4, 0)
+#define REG_ALC_ID_FIELD		GENMASK(7, 5)
+
+/* Bitfields of Transmission Delay Measurement register */
+#define REG_TRV_DLY_VAL			GENMASK(6, 0)
+
+/* Bitfields of Second Sampling Point Configuration register */
+#define REG_SSP_CFG_OFF			GENMASK(7, 0)
+#define REG_SSP_CFG_SRC			GENMASK(9, 8)
+#define REG_SSP_CFG_SAT			BIT(10)
+
+/* Bitfields of Receive Message Count register */
+#define REG_RX_FR_CNT_VAL		GENMASK(31, 0)
+
+/* Bitfields of Transmit Message Count register */
+#define REG_TX_FR_CNT_VAL		GENMASK(31, 0)
+
+/* Bitfields of Debug register */
+#define REG_DEBUG_STF_CNT		GENMASK(2, 0)
+#define REG_DEBUG_DSTF_CNT		GENMASK(5, 3)
+#define REG_DEBUG_PC_ARB		BIT(6)
+#define REG_DEBUG_PC_CON		BIT(7)
+#define REG_DEBUG_PC_DAT		BIT(8)
+#define REG_DEBUG_PC_STC		BIT(9)
+#define REG_DEBUG_PC_CRC		BIT(10)
+#define REG_DEBUG_PC_CRCD		BIT(11)
+#define REG_DEBUG_PC_ACK		BIT(12)
+#define REG_DEBUG_PC_ACKD		BIT(13)
+#define REG_DEBUG_PC_EOF		BIT(14)
+#define REG_DEBUG_PC_INT		BIT(15)
+#define REG_DEBUG_PC_SUSP		BIT(16)
+#define REG_DEBUG_PC_OVR		BIT(17)
+#define REG_DEBUG_PC_SOF		BIT(18)
+
+/* Bitfields of Timestamp register */
+#define REG_TS_TIMESTAMP		GENMASK(15, 0)
+#define REG_TS_PSC			GENMASK(24, 16)
+
+/* Bitfields of Fractional Divider Ratio register */
+#define REG_FRC_FRC_DBT			GENMASK(15, 8)
+#define REG_FRC_FRC_NBT			GENMASK(7, 0)
+
+/* Bitfields of Filter A Mask register */
+#define REG_FIL_A_MASK			GENMASK(28, 0)
+
+/* Bitfields of Filter A value register */
+#define REG_FIL_A_VAL			GENMASK(28, 0)
+
+/* Bitfields of Filter B Mask register */
+#define REG_FIL_B_MASK			GENMASK(28, 0)
+
+/* Bitfields of Filter B value register */
+#define REG_FIL_B_VAL			GENMASK(28, 0)
+
+/* Bitfields of Filter C Mask register */
+#define REG_FIL_C_MASK			GENMASK(28, 0)
+
+/* Bitfields of Filter C value register */
+#define REG_FIL_C_VAL			GENMASK(28, 0)
+
+/* Bitfields of Range Filter Low Threshold register */
+#define REG_FIL_R_LOW_VAL		GENMASK(28, 0)
+
+/* Bitfields of Range Filter High Threshold register */
+#define REG_FIL_R_HI_VAL		GENMASK(28, 0)
+
+/* Bitfields of Filter Control register */
+#define REG_FIL_CTRL_FANB		BIT(0)
+#define REG_FIL_CTRL_FANE		BIT(1)
+#define REG_FIL_CTRL_FAFB		BIT(2)
+#define REG_FIL_CTRL_FAFE		BIT(3)
+#define REG_FIL_CTRL_FBNB		BIT(4)
+#define REG_FIL_CTRL_FBNE		BIT(5)
+#define REG_FIL_CTRL_FBFB		BIT(6)
+#define REG_FIL_CTRL_FBFE		BIT(7)
+#define REG_FIL_CTRL_FCNB		BIT(8)
+#define REG_FIL_CTRL_FCNE		BIT(9)
+#define REG_FIL_CTRL_FCFB		BIT(10)
+#define REG_FIL_CTRL_FCFE		BIT(11)
+#define REG_FIL_CTRL_FRNB		BIT(12)
+#define REG_FIL_CTRL_FRNE		BIT(13)
+#define REG_FIL_CTRL_FRFB		BIT(14)
+#define REG_FIL_CTRL_FRFE		BIT(15)
+#define REG_FIL_CTRL_SFA		BIT(16)
+#define REG_FIL_CTRL_SFB		BIT(17)
+#define REG_FIL_CTRL_SFC		BIT(18)
+#define REG_FIL_CTRL_SFR		BIT(19)
+
+/* Bitfields of Receive Buffer Information register */
+#define REG_RX_MEM_INFO_BUFF_SIZE	GENMASK(12, 0)
+#define REG_RX_MEM_INFO_MEM_FREE	GENMASK(28, 16)
+
+/* Bitfields of Receive Buffer Pointer register */
+#define REG_RX_PTR_WPP			GENMASK(11, 0)
+#define REG_RX_PTR_RPP			GENMASK(27, 16)
+
+/* Bitfields of Receive Buffer Status register */
+#define REG_RX_STAT_RXE			BIT(0)
+#define REG_RX_STAT_RXF			BIT(1)
+#define REG_RX_STAT_RXMOF		BIT(2)
+#define REG_RX_STAT_RXFRC		GENMASK(14, 4)
+#define REG_RX_STAT_RTSOP		BIT(16)
+
+/* Bitfields of Receive Data register */
+#define REG_RX_DATA_VAL			GENMASK(31, 0)
+
+/* Bitfields of Transmit Buffer Status register */
+#define REG_TX_STAT_BRP			GENMASK(7, 0)
+#define REG_TX_STAT_TXS			GENMASK(10, 8)
+#define REG_TX_STAT_BS			GENMASK(31, 16)
+
+#define REG_TX_STAT_BS_TX_MASK_BASE	GENMASK(17, 16)
+
+/* Bitfields of Transmit Command register */
+#define REG_TX_CMD_BAR			GENMASK(7, 0)
+#define REG_TX_CMD_BCR			GENMASK(15, 8)
+#define REG_TX_CMD_BSC			GENMASK(23, 16)
+
+/* Bitfields of Transmit Buffer Selection register */
+#define REG_TX_SEL_BUF_SEL		GENMASK(3, 0)
+#define REG_TX_SEL_BUF_CNT		GENMASK(7, 4)
+
+/* Loongson CANFD Frame format */
+#define LOONGSON_CANFD_FRAME_META0	0x0
+#define LOONGSON_CANFD_FRAME_META1	0x4
+#define LOONGSON_CANFD_FRAME_DB_1	0x8
+
+/* Bitfields of FRAME META0 */
+#define REG_FRAME_META0_ID_EXT		GENMASK(17, 0)
+#define REG_FRAME_META0_ID_BASE		GENMASK(28, 18)
+#define REG_FRAME_META0_RTR		BIT(29)	/* Remote Transmission Request, only in CAN2.0 */
+/* Extended Identifier Type. 1: 29 bits ID, 0: 11 bits ID. */
+#define REG_FRAME_META0_XDT		BIT(30)
+#define REG_FRAME_META0_ESI		BIT(31)	/* Error State Indicator, only in CAN-FD */
+
+/* Bitfields of FRAME META1 */
+#define REG_FRAME_META1_TIMESTAMP	GENMASK(15, 0)
+#define REG_FRAME_META1_DLC		GENMASK(19, 16)	/* Data Length Code */
+#define REG_FRAME_META1_BRS		BIT(20)	/* Bit Rate Shift, Only in CANFD */
+/* Flexible Data-rate Format. 1: CANFD, 0: CAN2.0 */
+#define REG_FRAME_META1_FDF		BIT(21)
+#define REG_FRAME_META1_RWCNT		GENMASK(28, 24)	/* Read Word Counter */
+
+/* Bitfields of FRAME TEST */
+#define REG_FRAME_TEST_FSTC		BIT(0)
+#define REG_FRAME_TEST_FCRC		BIT(1)
+#define REG_FRAME_TEST_SDLC		BIT(2)
+#define REG_FRAME_TEST_TPRM		GENMASK(12, 8)
+
+#define DEV_NAME			"loongson_canfd"
+#define LOONGSON_CANFD_ID		0xBABE
+#define LOONGSON_CANFD_DW_BYTE		4
+#define LOONGSON_CANFD_TXBUF_NUM	8
+#define LOONGSON_CANFD_MAX_RTXTH	0xf
+
+/* Upper bound on the number of ISR drain iterations */
+#define LOONGSON_CANFD_IRQ_LOOP_MAX		10000
+
+/*
+ * 16-bit counter couples resolution and window length:
+ *     window = 2^16 * tick
+ *
+ * A 2 us tick yields ~131 ms, well above the worst case a frame can
+ * stay in the 32 KB RX ring before NAPI dequeues it.
+ */
+#define LOONGSON_CANFD_HWTS_TICK_NS		2000
+#define LOONGSON_CANFD_HWTS_COUNTER_BITS	16
+#define LOONGSON_CANFD_HWTS_SLEW_SHIFT		3
+#define LOONGSON_CANFD_HWTS_SLEW_BAND_NS	2000
+
+/**
+ * struct loongson_canfd_hwts - Hardware timestamp state
+ * @cc: Cycle counter abstracting the 16-bit timestamp counter
+ * @tc: Nanosecond time counter built on @cc, anchored to CLOCK_REALTIME
+ * @lock: Serializes access to @tc (NAPI, TX interrupt, chip start)
+ * @window_ns: Length of the unambiguous window, 2^16 tick periods
+ * @tick_ns: Achieved tick period in nanoseconds
+ * @psc: Prescaler value currently programmed into REG_TS_PSC
+ * @ready: Timestamping is configured and usable
+ */
+struct loongson_canfd_hwts {
+	struct cyclecounter	cc;
+	struct timecounter	tc;
+	spinlock_t		lock;	/* protect the tc context */
+	u64			window_ns;
+	u32			tick_ns;
+	u32			psc;
+	bool			ready;
+};
+
+/**
+ * struct loongson_canfd_rx_desc - Parsed on-wire frame descriptor
+ * @hdr0: First header word: ID / RTR / XDT / ESI
+ * @hdr1: Second header word: timestamp / DLC / BRS / FDF / RWCNT
+ * @rwcnt: Total 32-bit words the frame occupies in the ring
+ * @dbcnt: Payload size in bytes, (rwcnt - 2) * 4
+ * @is_can_fd: True if the FDF bit is set
+ * @is_rtr: True if the RTR bit is set (Classic CAN only)
+ */
+struct loongson_canfd_rx_desc {
+	u32	hdr0;
+	u32	hdr1;
+	u32	rwcnt;
+	u32	dbcnt;
+	bool	is_can_fd;
+	bool	is_rtr;
+};
+
+/**
+ * struct loongson_canfd_priv - Driver private data
+ * @can: CAN private data structure (must be first member)
+ * @napi: NAPI structure
+ * @regmap: Regmap of the CAN device
+ * @mem_res: Pointer to the CAN device resource (MMIO region)
+ * @tx_lock: Lock for synchronizing TX interrupt handling
+ * @stats_lock: Lock for protecting netdev statistics
+ * @hwts: Hardware timestamp state
+ */
+struct loongson_canfd_priv {
+	struct can_priv		can;
+	struct napi_struct	napi;
+	struct regmap		*regmap;
+	struct resource		*mem_res;
+	spinlock_t		tx_lock;	/* protect the sending queue */
+	spinlock_t		stats_lock;	/* protect the ndev->stats */
+	struct loongson_canfd_hwts	hwts;
+};
+
+/**
+ * enum loongson_canfd_txbuf_sts - Status of TX buffer
+ * @TX_BS_IDLE: No transmission or processed.
+ * @TX_BS_VALID: Transmission successful.
+ * @TX_BS_FAIL: Transmission failed (retry limit exceeded).
+ * @TX_BS_CANCEL: Transmission cancelled.
+ */
+enum loongson_canfd_txbuf_sts {
+	TX_BS_IDLE	= 0x0,
+	TX_BS_VALID	= 0x1,
+	TX_BS_FAIL	= 0x2,
+	TX_BS_CANCEL	= 0x3
+};
+
+/**
+ * enum loongson_canfd_txbuf_cmd - Commands for TX buffer
+ * @TXT_CMD_ADD: Add buffer request (start transmission)
+ * @TXT_CMD_CANCEL: Cancel buffer request
+ * @TXT_CMD_SR_CLEAN: Clear send completion record
+ */
+enum loongson_canfd_txbuf_cmd {
+	TXT_CMD_ADD		= 0x01,
+	TXT_CMD_CANCEL		= 0x02,
+	TXT_CMD_SR_CLEAN	= 0x03
+};
+
+static const struct can_bittiming_const loongson_canfd_bit_timing = {
+	.name		= DEV_NAME,
+	.tseg1_min	= 2,
+	.tseg1_max	= 190,
+	.tseg2_min	= 2,
+	.tseg2_max	= 63,
+	.sjw_max	= 31,
+	.brp_min	= 1,
+	.brp_max	= 15,
+	.brp_inc	= 1,
+};
+
+static const struct can_bittiming_const loongson_canfd_bit_timing_data = {
+	.name		= DEV_NAME,
+	.tseg1_min	= 2,
+	.tseg1_max	= 190,
+	.tseg2_min	= 2,
+	.tseg2_max	= 63,
+	.sjw_max	= 31,
+	.brp_min	= 1,
+	.brp_max	= 255,
+	.brp_inc	= 1,
+};
+
+static const struct can_tdc_const loongson_canfd_tdc_data = {
+	.tdcv_min	= 0,
+	.tdcv_max	= 0,
+	.tdco_min	= 0,
+	.tdco_max	= 255,
+	.tdcf_min	= 0,
+	.tdcf_max	= 0,
+};
+
+/* Return @field when @cond is true, 0 otherwise. */
+#define LOONGSON_CANFD_FIELD_IF(cond, field) (-(!!(cond)) & (field))
+
+/**
+ * loongson_canfd_enabled() - Check if controller is enabled
+ * @priv: Driver private data
+ *
+ * Return: true if enabled, false otherwise.
+ */
+static bool loongson_canfd_enabled(struct loongson_canfd_priv *priv)
+{
+	return !!regmap_test_bits(priv->regmap, LOONGSON_CANFD_CONF, REG_CONF_ENA);
+}
+
+/**
+ * loongson_canfd_disable_interrupts() - Disable all interrupts
+ * @priv: Driver private data
+ */
+static void loongson_canfd_disable_interrupts(struct loongson_canfd_priv *priv)
+{
+	/* Clear all ENA bits, then set all MASK bits */
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, REG_INT_ENA_CLR);
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, REG_INT_MASK_SET);
+}
+
+/**
+ * loongson_canfd_read_rx_word_fifo() - Read RX data in poll mode
+ * @priv: Driver private data
+ *
+ * Return: The next 32-bit data word from the RX data register.
+ */
+static u32 loongson_canfd_read_rx_word_fifo(struct loongson_canfd_priv *priv)
+{
+	u32 data;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_RX_DATA, &data);
+	return data;
+}
+
+/**
+ * loongson_canfd_rx_is_empty_fifo() - Poll mode: hardware FIFO empty check
+ * @priv: Driver private data
+ *
+ * Uses both RXE and RXFRC so a transient RXE during FIFO refill is not
+ * mistaken for an empty FIFO.
+ *
+ * Return: true if no frame is pending and RXE is set.
+ */
+static bool loongson_canfd_rx_is_empty_fifo(struct loongson_canfd_priv *priv)
+{
+	u32 rx_stat, framecnt;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
+	framecnt = FIELD_GET(REG_RX_STAT_RXFRC, rx_stat);
+
+	if (framecnt)
+		return false;
+
+	/* RXE may transiently assert while the FIFO is being refilled */
+	return !!(rx_stat & REG_RX_STAT_RXE);
+}
+
+/**
+ * loongson_canfd_hwts_read_cycles() - Read the free-running timestamp counter
+ * @cc: Cycle counter whose read callback is being invoked
+ *
+ * Reads LOONGSON_CANFD_TS and extracts the 16-bit counter field (bits 15:0).
+ * The register is marked volatile in loongson_canfd_volatile_reg(), so
+ * regmap_read() bypasses the cache and returns a live value.
+ *
+ * Return: Current value of the 16-bit timestamp counter.
+ */
+static u64 loongson_canfd_hwts_read_cycles(struct cyclecounter *cc)
+{
+	struct loongson_canfd_hwts *hwts =
+		container_of(cc, struct loongson_canfd_hwts, cc);
+	struct loongson_canfd_priv *priv =
+		container_of(hwts, struct loongson_canfd_priv, hwts);
+	u32 val;
+
+	/* REG_TS is volatile, so this returns a live counter value */
+	regmap_read(priv->regmap, LOONGSON_CANFD_TS, &val);
+
+	return FIELD_GET(REG_TS_TIMESTAMP, val);
+}
+
+/**
+ * loongson_canfd_hwts_anchor() - Refresh the nanosecond epoch
+ * @priv: Driver private data
+ *
+ * Calls timecounter_read() so a single wrap can be followed, then
+ * nudges the epoch toward CLOCK_REALTIME. A deviation of half a window
+ * or more means the epoch is worthless and is re-initialized instead.
+ *
+ * Caller must hold @hwts.lock.
+ */
+static void loongson_canfd_hwts_anchor(struct loongson_canfd_priv *priv)
+{
+	struct loongson_canfd_hwts *hwts = &priv->hwts;
+	s64 dev;
+
+	timecounter_read(&hwts->tc);
+
+	if (!hwts->window_ns)
+		return;
+
+	dev = ktime_get_real_ns() - (s64)hwts->tc.nsec;
+
+	/* Whole windows lost: re-lock, do not slew (stamps must stay monotonic) */
+	if (dev >= (s64)hwts->window_ns / 2 ||
+	    dev <= -(s64)hwts->window_ns / 2) {
+		timecounter_init(&hwts->tc, &hwts->cc, ktime_get_real_ns());
+		return;
+	}
+
+	if (dev > -LOONGSON_CANFD_HWTS_SLEW_BAND_NS &&
+	    dev < LOONGSON_CANFD_HWTS_SLEW_BAND_NS)
+		return;
+
+	timecounter_adjtime(&hwts->tc, dev >> LOONGSON_CANFD_HWTS_SLEW_SHIFT);
+}
+
+/**
+ * loongson_canfd_hwts_ts() - Convert a raw 16-bit stamp to a ktime_t
+ * @priv: Driver private data
+ * @raw: Raw 16-bit timestamp taken from the frame
+ *
+ * Frames are always stamped before the driver reads them, so the
+ * result is folded into the most recent past window, making the
+ * reconstruction exact for any frame age below one full window.
+ *
+ * Return: ktime_t of @raw, in CLOCK_REALTIME nanoseconds.
+ */
+static ktime_t loongson_canfd_hwts_ts(struct loongson_canfd_priv *priv, u32 raw)
+{
+	struct loongson_canfd_hwts *hwts = &priv->hwts;
+	s64 age, ts;
+
+	guard(spinlock_irqsave)(&hwts->lock);
+
+	loongson_canfd_hwts_anchor(priv);
+
+	ts = (s64)timecounter_cyc2time(&hwts->tc, raw);
+
+	age = (s64)hwts->tc.nsec - ts;
+	if (age < 0)
+		age += (s64)hwts->window_ns;
+
+	return ns_to_ktime((s64)hwts->tc.nsec - age);
+}
+
+/**
+ * loongson_canfd_hwts_now() - Timestamp of the current moment
+ * @priv: Driver private data
+ *
+ * Samples the counter before converting, so the result is never later
+ * than the moment of the call.
+ *
+ * Return: ktime_t of now, in CLOCK_REALTIME nanoseconds.
+ */
+static ktime_t loongson_canfd_hwts_now(struct loongson_canfd_priv *priv)
+{
+	struct loongson_canfd_hwts *hwts = &priv->hwts;
+
+	/* Sample first: the counter is the earlier of the two instants */
+	return loongson_canfd_hwts_ts(priv, (u32)hwts->cc.read(&hwts->cc));
+}
+
+/**
+ * loongson_canfd_hwts_setup() - Program the prescaler and start the epoch
+ * @ndev: Network device
+ *
+ * The 16-bit free-running counter is extended to 64-bit nanoseconds
+ * via a cyclecounter/timecounter pair anchored to CLOCK_REALTIME. On
+ * failure @hwts.ready is left false; the controller can still run,
+ * just without timestamping.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int loongson_canfd_hwts_setup(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct loongson_canfd_hwts *hwts = &priv->hwts;
+	u32 tick_hz, clk = priv->can.clock.freq;
+	u64 frac = 0, div;
+	int ret;
+
+	/* Disable timestamping until the new setup succeeds */
+	WRITE_ONCE(hwts->ready, false);
+
+	if (!clk) {
+		netdev_warn(ndev, "Zero clock, HW timestamp unavailable\n");
+		return -EINVAL;
+	}
+
+	/* tick = (psc + 1) / clk, aim for LOONGSON_CANFD_HWTS_TICK_NS */
+	div = DIV_ROUND_CLOSEST_ULL((u64)LOONGSON_CANFD_HWTS_TICK_NS * clk,
+				    NSEC_PER_SEC);
+	div = clamp_t(u64, div, 1, FIELD_MAX(REG_TS_PSC) + 1ULL);
+
+	hwts->psc = div - 1;
+	hwts->tick_ns = DIV_ROUND_CLOSEST_ULL(div * NSEC_PER_SEC, clk);
+
+	tick_hz = clk / (u32)div;
+	if (!tick_hz) {
+		netdev_warn(ndev, "Clock too slow for HW timestamp (%u Hz)\n",
+			    clk);
+		return -EINVAL;
+	}
+
+	hwts->cc.read = loongson_canfd_hwts_read_cycles;
+	hwts->cc.mask = CYCLECOUNTER_MASK(LOONGSON_CANFD_HWTS_COUNTER_BITS);
+	clocks_calc_mult_shift(&hwts->cc.mult, &hwts->cc.shift,
+			       tick_hz, NSEC_PER_SEC, 60);
+
+	hwts->window_ns = cyclecounter_cyc2ns(&hwts->cc,
+					      hwts->cc.mask + 1, 0, &frac);
+
+	/* Update only the prescaler bits, leave the counter alone */
+	ret = regmap_update_bits(priv->regmap, LOONGSON_CANFD_TS, REG_TS_PSC,
+				 FIELD_PREP(REG_TS_PSC, hwts->psc));
+	if (ret) {
+		netdev_err(ndev, "Failed to program TS prescaler: %d\n", ret);
+		return ret;
+	}
+
+	scoped_guard(spinlock_irqsave, &hwts->lock) {
+		timecounter_init(&hwts->tc, &hwts->cc, ktime_get_real_ns());
+	}
+
+	WRITE_ONCE(hwts->ready, true);
+
+	netdev_dbg(ndev, "HW timestamp: psc=%u, tick=%u ns, window=%llu ns\n",
+		   hwts->psc, hwts->tick_ns, hwts->window_ns);
+
+	return 0;
+}
+
+/**
+ * loongson_canfd_reset() - Issue soft reset to controller
+ * @ndev: Network device
+ *
+ * The reset bit does not self-clear on this silicon; the mode write
+ * below re-programs the register, so a timeout is only logged.
+ */
+static void loongson_canfd_reset(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	int ret, val;
+
+	regmap_write(priv->regmap, LOONGSON_CANFD_MODE, REG_MODE_RST);
+
+	ret = regmap_read_poll_timeout(priv->regmap, LOONGSON_CANFD_MODE,
+				       val, !(val & REG_MODE_RST),
+				       10, 10 * USEC_PER_MSEC);
+	if (ret)
+		dev_dbg(ndev->dev.parent,
+			"Soft reset did not self-clear: %d\n", ret);
+
+	regmap_write(priv->regmap, LOONGSON_CANFD_MODE, REG_MODE_RXBAM | REG_MODE_BUFM);
+}
+
+/**
+ * loongson_canfd_set_btr() - Set CAN bit timing registers
+ * @ndev: Network device
+ * @bt: Bit timing structure
+ * @nominal: true for nominal timing, false for data timing
+ */
+static void loongson_canfd_set_btr(struct net_device *ndev,
+				   struct can_bittiming *bt, bool nominal)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 prop = bt->prop_seg, phase1 = bt->phase_seg1;
+	u32 ph1_max = FIELD_MAX(REG_BTR_PH1);
+	u32 btr = 0;
+
+	/* PROP is 7 bits but PH1 only 6; move the excess into PROP */
+	if (phase1 > ph1_max) {
+		prop += phase1 - ph1_max;
+		phase1 = ph1_max;
+	}
+
+	if (nominal) {
+		btr = FIELD_PREP(REG_BTR_PROP, prop) |
+		      FIELD_PREP(REG_BTR_PH1, phase1) |
+		      FIELD_PREP(REG_BTR_PH2, bt->phase_seg2) |
+		      FIELD_PREP(REG_BTR_BRP, bt->brp) |
+		      FIELD_PREP(REG_BTR_SJW, bt->sjw);
+
+		regmap_write(priv->regmap, LOONGSON_CANFD_BTR_NORM, btr);
+	} else {
+		btr = FIELD_PREP(REG_BTR_FD_PROP, prop) |
+		      FIELD_PREP(REG_BTR_FD_PH1, phase1) |
+		      FIELD_PREP(REG_BTR_FD_PH2, bt->phase_seg2) |
+		      FIELD_PREP(REG_BTR_FD_BRP, bt->brp) |
+		      FIELD_PREP(REG_BTR_FD_SJW, bt->sjw);
+
+		regmap_write(priv->regmap, LOONGSON_CANFD_BTR_FD, btr);
+	}
+}
+
+/**
+ * loongson_canfd_set_bittiming() - Set nominal bit timing
+ * @ndev: Network device
+ *
+ * Return: 0 on success, negative on error.
+ */
+static int loongson_canfd_set_bittiming(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct can_bittiming *bt = &priv->can.bittiming;
+
+	loongson_canfd_set_btr(ndev, bt, true);
+
+	return 0;
+}
+
+/**
+ * loongson_canfd_set_secondary_sample_point() - Configure secondary sample point
+ * @ndev: Network device
+ *
+ * Return: 0 on success, -EBUSY if the controller is enabled.
+ */
+static int loongson_canfd_set_secondary_sample_point(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 ctrlmode = priv->can.ctrlmode;
+	u32 ssp_cfg;
+
+	if (loongson_canfd_enabled(priv)) {
+		netdev_err(ndev, "Cannot set SSP while CAN is enabled\n");
+		return -EBUSY;
+	}
+
+	if (ctrlmode & CAN_CTRLMODE_TDC_MANUAL)
+		/* Manual TDC: 2'd2, delay = SSP_OFF */
+		ssp_cfg = FIELD_PREP(REG_SSP_CFG_SRC, 2) |
+			  FIELD_PREP(REG_SSP_CFG_OFF, priv->can.fd.tdc.tdco);
+	else if (ctrlmode & CAN_CTRLMODE_TDC_AUTO)
+		/* Auto TDC: 2'd0, delay = TRV_DLY + SSP_OFF */
+		ssp_cfg = FIELD_PREP(REG_SSP_CFG_SRC, 0) |
+			  FIELD_PREP(REG_SSP_CFG_OFF, priv->can.fd.tdc.tdco);
+	else
+		/* TDC Disabled: 2'd1, use standard sample point */
+		ssp_cfg = FIELD_PREP(REG_SSP_CFG_SRC, 1) |
+			  FIELD_PREP(REG_SSP_CFG_OFF, 0);
+
+	return regmap_write(priv->regmap, LOONGSON_CANFD_SSP_CFG, ssp_cfg);
+}
+
+/**
+ * loongson_canfd_set_data_bittiming() - Set data bit timing and SSP
+ * @ndev: Network device
+ *
+ * BTR_FD and SSP_CFG are refreshed together because the SSP position
+ * depends on the data bit time.
+ *
+ * Return: 0 on success, negative on error.
+ */
+static int loongson_canfd_set_data_bittiming(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct can_bittiming *dbt = &priv->can.fd.data_bittiming;
+	int ret;
+
+	loongson_canfd_set_btr(ndev, dbt, false);
+
+	ret = loongson_canfd_set_secondary_sample_point(ndev);
+	if (ret < 0)
+		return ret;
+
+	return 0;
+}
+
+/**
+ * loongson_canfd_get_auto_tdcv() - Get transmitter delay compensation value
+ * @ndev: Network device
+ * @tdcv: Pointer to store TDCV value
+ *
+ * Return: 0 on success, -EOPNOTSUPP on CAN_CTRLMODE_TDC_AUTO is not set.
+ */
+static int loongson_canfd_get_auto_tdcv(const struct net_device *ndev, u32 *tdcv)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 val;
+
+	if (!(priv->can.ctrlmode & CAN_CTRLMODE_TDC_AUTO))
+		return -EOPNOTSUPP;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_TRV_DLY, &val);
+	*tdcv = FIELD_GET(REG_TRV_DLY_VAL, val);
+
+	/* Hardware measurement is complete only when TRV_DLY is not 0 */
+	if (!*tdcv)
+		return -EAGAIN;
+
+	return 0;
+}
+
+/**
+ * loongson_canfd_apply_ctrlmode() - Apply CAN ctrlmode to MODE/CONF
+ * @priv: Driver private data
+ */
+static void loongson_canfd_apply_ctrlmode(struct loongson_canfd_priv *priv)
+{
+	u32 ctrlmode = priv->can.ctrlmode;
+	u32 mode, conf;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_MODE, &mode);
+
+	mode &= ~(REG_MODE_BMM | REG_MODE_FDE | REG_MODE_ACF | REG_MODE_TSTM);
+	mode |= REG_MODE_ITSM | REG_MODE_RTSOP;
+	mode |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_LISTENONLY,
+					REG_MODE_BMM);
+	mode |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_FD,
+					REG_MODE_FDE);
+	mode |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_PRESUME_ACK,
+					REG_MODE_ACF);
+
+	regmap_write(priv->regmap, LOONGSON_CANFD_MODE, mode);
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_CONF, &conf);
+
+	conf &= ~(REG_CONF_ILBP | REG_CONF_NISOFD |
+		  REG_CONF_RTRLE | REG_CONF_RTRTH);
+
+	/* RTRLE always on; ONE_SHOT only decides RTRTH */
+	conf |= REG_CONF_RTRLE;
+	conf |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_LOOPBACK,
+					REG_CONF_ILBP);
+	conf |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_FD_NON_ISO,
+					REG_CONF_NISOFD);
+	conf |= LOONGSON_CANFD_FIELD_IF(!(ctrlmode & CAN_CTRLMODE_ONE_SHOT),
+					FIELD_PREP(REG_CONF_RTRTH,
+						   LOONGSON_CANFD_MAX_RTXTH));
+
+	regmap_write(priv->regmap, LOONGSON_CANFD_CONF, conf);
+}
+
+/**
+ * loongson_canfd_chip_start() - Start the CAN controller
+ * @ndev: Network device
+ *
+ * Configures bit timing, SSP, interrupt enables, and enables the controller.
+ *
+ * Return: 0 on success, negative error on failure.
+ */
+static int loongson_canfd_chip_start(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 int_ena, int_msk;
+	int ret;
+
+	ret = loongson_canfd_set_bittiming(ndev);
+	if (ret < 0)
+		return ret;
+
+	/* Programs BTR_FD and SSP_CFG together */
+	ret = loongson_canfd_set_data_bittiming(ndev);
+	if (ret < 0)
+		return ret;
+
+	loongson_canfd_apply_ctrlmode(priv);
+
+	int_ena = REG_INT_STAT_RBNEI | REG_INT_STAT_TXBHCI |
+		  REG_INT_STAT_EWLI  | REG_INT_STAT_FCSI   |
+		  REG_INT_STAT_DOI;
+
+	if (priv->can.ctrlmode & CAN_CTRLMODE_BERR_REPORTING)
+		int_ena |= REG_INT_STAT_ALI | REG_INT_STAT_BEI;
+
+	int_msk = REG_INT_MASK_SET & ~int_ena;
+
+	/* Clear stale mask/ena bits left by disable_interrupts() */
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, REG_INT_MASK_CLR);
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, REG_INT_ENA_CLR);
+
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, int_msk);
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, int_ena);
+
+	loongson_canfd_hwts_setup(ndev);
+
+	priv->can.state = CAN_STATE_ERROR_ACTIVE;
+
+	regmap_update_bits(priv->regmap, LOONGSON_CANFD_CONF, REG_CONF_ENA,
+			   REG_CONF_ENA);
+
+	return 0;
+}
+
+/**
+ * loongson_canfd_chip_stop() - Stop the controller
+ * @ndev: Network device
+ */
+static void loongson_canfd_chip_stop(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	loongson_canfd_disable_interrupts(priv);
+	regmap_update_bits(priv->regmap, LOONGSON_CANFD_CONF, REG_CONF_ENA, 0);
+
+	priv->can.state = CAN_STATE_STOPPED;
+
+	/* The timestamp counter only runs while the controller is enabled */
+	WRITE_ONCE(priv->hwts.ready, false);
+}
+
+/**
+ * loongson_canfd_do_set_mode() - Change operation mode
+ * @ndev: Network device
+ * @mode: Mode to set
+ *
+ * Return: 0 on success, negative error on failure.
+ */
+static int loongson_canfd_do_set_mode(struct net_device *ndev, enum can_mode mode)
+{
+	int ret;
+
+	switch (mode) {
+	case CAN_MODE_START:
+		loongson_canfd_reset(ndev);
+
+		ret = loongson_canfd_chip_start(ndev);
+		if (ret < 0) {
+			netdev_err(ndev, "Failed to start chip\n");
+			return ret;
+		}
+
+		netif_wake_queue(ndev);
+		break;
+	default:
+		ret = -EOPNOTSUPP;
+		break;
+	}
+
+	return ret;
+}
+
+/**
+ * loongson_canfd_txbuf_freed() - Check if at least one TX buffer is free
+ * @priv: Driver private data
+ *
+ * Examines the TX buffer pending status (BRP bits). If any of the 8 buffers
+ * is not pending, it indicates a free buffer.
+ *
+ * Return: true if at least one TX buffer is free, false if all pending.
+ */
+static bool loongson_canfd_txbuf_freed(struct loongson_canfd_priv *priv)
+{
+	return !regmap_test_bits(priv->regmap, LOONGSON_CANFD_TX_STAT, REG_TX_STAT_BRP);
+}
+
+/**
+ * loongson_canfd_get_txbuf_sts() - Get status of a TX buffer
+ * @priv: Driver private data
+ * @buf_id: Buffer index (0-based, 0..7)
+ *
+ * Reads the 2-bit BS field for @buf_id from TX_STAT. The field is located
+ * at bits (16 + 2 * buf_id) .. (17 + 2 * buf_id).
+ *
+ * Return: Status of the specified TX buffer as an enum value.
+ */
+static enum loongson_canfd_txbuf_sts
+loongson_canfd_get_txbuf_sts(struct loongson_canfd_priv *priv, u8 buf_id)
+{
+	u32 sts, mask;
+
+	/* BS field for buf_id lives at bits (16 + 2*buf_id):(17 + 2*buf_id) */
+	mask = REG_TX_STAT_BS_TX_MASK_BASE << (2 * buf_id);
+	regmap_read(priv->regmap, LOONGSON_CANFD_TX_STAT, &sts);
+
+	return field_get(mask, sts);
+}
+
+/**
+ * loongson_canfd_txbuf_is_writable() - Check if a TX buffer is writable
+ * @priv: Driver private data
+ * @buf_id: Buffer index (0-based)
+ *
+ * A buffer accepts a new frame only if:
+ *   - its BRP bit is clear (not pending), and
+ *   - its 2-bit BS field is TX_BS_IDLE (previous frame finished).
+ *
+ * Return: true if the buffer is in a writable state, false otherwise.
+ */
+static bool loongson_canfd_txbuf_is_writable(struct loongson_canfd_priv *priv, u8 buf_id)
+{
+	enum loongson_canfd_txbuf_sts bs;
+
+	/* Writable only if BS is idle and BRP bit is clear */
+	bs = loongson_canfd_get_txbuf_sts(priv, buf_id);
+	if (bs)
+		return false;
+
+	return !regmap_test_bits(priv->regmap, LOONGSON_CANFD_TX_STAT, BIT(buf_id));
+}
+
+/**
+ * loongson_canfd_set_txbuf_cmd() - Apply command on TX buffer
+ * @ndev: Network device
+ * @cmd: Command to apply (TXT_CMD_ADD, TXT_CMD_CANCEL, TXT_CMD_SR_CLEAN)
+ * @buf_id: Buffer index (0-based)
+ *
+ * Writes the appropriate command bit (BAR, BCR, or BSC) for @buf_id into
+ * the TX_CMD register. Each command field is 8 bits wide, so the buffer
+ * index is encoded as BIT(buf_id).
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int loongson_canfd_set_txbuf_cmd(struct net_device *ndev,
+					enum loongson_canfd_txbuf_cmd cmd, u8 buf_id)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 mask;
+
+	switch (cmd) {
+	case TXT_CMD_ADD:
+		mask = REG_TX_CMD_BAR;
+		break;
+	case TXT_CMD_CANCEL:
+		mask = REG_TX_CMD_BCR;
+		break;
+	case TXT_CMD_SR_CLEAN:
+		mask = REG_TX_CMD_BSC;
+		break;
+	default:
+		netdev_err(ndev, "Unknown command id: %x\n", cmd);
+		return -EINVAL;
+	}
+
+	/* Each command field is 8 bits wide: buffer index is BIT(buf_id) */
+	return regmap_write(priv->regmap, LOONGSON_CANFD_TX_CMD,
+			    field_prep(mask, BIT(buf_id)));
+}
+
+/**
+ * loongson_canfd_insert_frame() - Insert a frame into TX buffer
+ * @priv: Driver private data
+ * @cf: CAN frame to transmit
+ * @buf: Buffer index (0-based)
+ * @isfdf: true for CAN FD frame, false for CAN 2.0
+ *
+ * Writes the frame header (META0, META1) and payload into the selected
+ * TX buffer. For RTR frames, no payload is written.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int loongson_canfd_insert_frame(struct loongson_canfd_priv *priv,
+				       const struct canfd_frame *cf, u8 buf,
+				       bool isfdf)
+{
+	u32 frame_hdr0, frame_hdr1 = 0;
+	int ret;
+
+	if (buf >= LOONGSON_CANFD_TXBUF_NUM)
+		return -EINVAL;
+
+	if (!loongson_canfd_txbuf_is_writable(priv, buf))
+		return -EBUSY;
+
+	if (cf->can_id & CAN_EFF_FLAG) {
+		frame_hdr0 = cf->can_id & CAN_EFF_MASK;
+		frame_hdr0 |= REG_FRAME_META0_XDT;
+	} else {
+		frame_hdr0 = FIELD_PREP(REG_FRAME_META0_ID_BASE,
+					cf->can_id & CAN_SFF_MASK);
+	}
+
+	if (cf->can_id & CAN_RTR_FLAG)
+		frame_hdr0 |= REG_FRAME_META0_RTR;
+
+	if (isfdf) {
+		frame_hdr1 = REG_FRAME_META1_FDF;
+		if (cf->flags & CANFD_BRS)
+			frame_hdr1 |= REG_FRAME_META1_BRS;
+		frame_hdr1 |= FIELD_PREP(REG_FRAME_META1_DLC,
+					 can_fd_len2dlc(cf->len));
+	} else {
+		/*
+		 * Classic CAN: can_get_cc_dlc() honours CC_LEN8_DLC so
+		 * len8_dlc can request DLC 9..15 for len == 8.
+		 */
+		struct can_frame *ccf = (struct can_frame *)cf;
+
+		frame_hdr1 |= FIELD_PREP(REG_FRAME_META1_DLC,
+					 can_get_cc_dlc(ccf, priv->can.ctrlmode));
+	}
+
+	ret = regmap_write(priv->regmap, LOONGSON_CANFD_TX_SEL, buf);
+	if (ret)
+		return ret;
+
+	ret = regmap_write(priv->regmap,
+			   LOONGSON_CANFD_TX_DATA_1 + LOONGSON_CANFD_FRAME_META0,
+			   frame_hdr0);
+	if (ret)
+		return ret;
+
+	ret = regmap_write(priv->regmap,
+			   LOONGSON_CANFD_TX_DATA_1 + LOONGSON_CANFD_FRAME_META1,
+			   frame_hdr1);
+	if (ret)
+		return ret;
+
+	/* RTR frames carry no payload */
+	if (cf->can_id & CAN_RTR_FLAG)
+		return 0;
+
+	for (unsigned int i = 0; i < cf->len; i += LOONGSON_CANFD_DW_BYTE) {
+		unsigned int remain = cf->len - i;
+		u32 word;
+
+		if (remain >= LOONGSON_CANFD_DW_BYTE) {
+			word = get_unaligned_le32(cf->data + i);
+		} else {
+			u8 buf[LOONGSON_CANFD_DW_BYTE] = { 0 };
+
+			memcpy(buf, cf->data + i, remain);
+			word = get_unaligned_le32(buf);
+		}
+
+		ret = regmap_write(priv->regmap,
+				   LOONGSON_CANFD_TX_DATA_1 +
+				   LOONGSON_CANFD_FRAME_DB_1 + i,
+				   word);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+/**
+ * loongson_canfd_find_free_txbuf() - Find a TX buffer ready for immediate write
+ * @priv: Driver private data
+ *
+ * The hardware requires both conditions to be met for a buffer to accept
+ * a new frame:
+ *   - its BRP bit indicates the buffer is not pending (tx_brp bit clear)
+ *   - its 2-bit BS field indicates the previous frame finished (tx_bs bits clear)
+ *
+ * Return: buffer index 0..LOONGSON_CANFD_TXBUF_NUM-1, or -1 if none free.
+ */
+static int loongson_canfd_find_free_txbuf(struct loongson_canfd_priv *priv)
+{
+	unsigned long tx_brp;
+	u32 tx_stat, i;
+	u16 tx_bs;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_TX_STAT, &tx_stat);
+	tx_brp = FIELD_GET(REG_TX_STAT_BRP, tx_stat);
+	tx_bs  = FIELD_GET(REG_TX_STAT_BS, tx_stat);
+
+	/* Need both BRP clear and BS idle; skip slots still echoing */
+	for_each_clear_bit(i, &tx_brp, LOONGSON_CANFD_TXBUF_NUM) {
+		u32 mask = 0x3 << (i * 2);
+
+		if (!(tx_bs & mask)) {
+			if (priv->can.echo_skb[i])
+				continue;
+			return i;
+		}
+	}
+
+	return -1;
+}
+
+/**
+ * loongson_canfd_hdr_plausible() - Decide whether a header pair is real
+ * @hdr0: META0 word
+ * @hdr1: META1 word
+ *
+ * Cross-checks META0/META1 field widths against protocol limits and
+ * validates RWCNT against the frame type. The RTR check must stay
+ * here: this is also used as a DMA resync probe and needs the full
+ * discriminator to avoid mistaking a payload word for a header.
+ *
+ * Return: true if the pair is consistent with a real frame header.
+ */
+static bool loongson_canfd_hdr_plausible(u32 hdr0, u32 hdr1)
+{
+	u32 rwcnt  = FIELD_GET(REG_FRAME_META1_RWCNT, hdr1);
+	u32 dlc    = FIELD_GET(REG_FRAME_META1_DLC,   hdr1);
+	bool isfd  = !!(hdr1 & REG_FRAME_META1_FDF);
+	bool isrtr = !!(hdr0 & REG_FRAME_META0_RTR);
+	u32 payload_len, expected_rwcnt;
+
+	/* 2 header words + payload, within CAN FD limits */
+	if (rwcnt < 2 || rwcnt > 2 + CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE)
+		return false;
+
+	/* RTR is a Classic CAN concept */
+	if (isfd && isrtr)
+		return false;
+
+	/* Standard ID fits in 11 bits */
+	if (!(hdr0 & REG_FRAME_META0_XDT) &&
+	    FIELD_GET(REG_FRAME_META0_ID_BASE, hdr0) > CAN_SFF_MASK)
+		return false;
+
+	/* RTR frames carry no payload */
+	if (isrtr)
+		return rwcnt == 2;
+
+	/*
+	 * RWCNT and DLC are independent fields; a real data frame always
+	 * satisfies rwcnt == 2 + DIV_ROUND_UP(payload_len, 4).
+	 */
+	if (isfd)
+		payload_len = can_fd_dlc2len(dlc);
+	else
+		payload_len = min_t(u32, dlc, CAN_MAX_DLEN);
+
+	expected_rwcnt = 2 + DIV_ROUND_UP(payload_len, LOONGSON_CANFD_DW_BYTE);
+	return rwcnt == expected_rwcnt;
+}
+
+/**
+ * loongson_canfd_rx_note_bad() - Account a malformed frame and resync the FIFO
+ * @ndev: Network device
+ * @rwcnt: Raw RWCNT field from the rejected frame's META1 word
+ *
+ * @rwcnt may be a sentinel from rx_hdr_valid(), so the payload drain
+ * is clamped to the maximum legal CAN FD size.
+ */
+static void loongson_canfd_rx_note_bad(struct net_device *ndev, u32 rwcnt)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 words, data;
+
+	ndev->stats.rx_dropped++;
+
+	words = rwcnt > 2 ?
+		min_t(u32, rwcnt - 2,
+		      CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE) : 0;
+	while (words--)
+		regmap_read(priv->regmap, LOONGSON_CANFD_RX_DATA, &data);
+}
+
+/**
+ * loongson_canfd_rx_hdr_valid() - Validate a frame header pair
+ * @ndev: Network device
+ * @d: Output descriptor, filled with all parsed fields
+ *
+ * Reads and sanity-checks the two header words. The caller must drain
+ * the remaining payload words when false is returned.
+ *
+ * Return: true if the header is valid, false otherwise.
+ */
+static bool loongson_canfd_rx_hdr_valid(struct net_device *ndev,
+					struct loongson_canfd_rx_desc *d)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	d->hdr0 = loongson_canfd_read_rx_word_fifo(priv);
+	d->hdr1 = loongson_canfd_read_rx_word_fifo(priv);
+
+	d->rwcnt = FIELD_GET(REG_FRAME_META1_RWCNT, d->hdr1);
+	if (d->rwcnt < 2) {
+		/* Hand the caller a worst-case drain length */
+		d->rwcnt = 2 + CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE;
+		return false;
+	}
+
+	d->dbcnt = (d->rwcnt - 2) * LOONGSON_CANFD_DW_BYTE;
+	if (d->dbcnt > CANFD_MAX_DLEN)
+		return false;
+
+	d->is_can_fd = !!(d->hdr1 & REG_FRAME_META1_FDF);
+	d->is_rtr    = !!(d->hdr0 & REG_FRAME_META0_RTR);
+
+	/* Classic CAN cannot exceed 8 bytes; oversized dbcnt would overflow */
+	if (!d->is_can_fd && d->dbcnt > CAN_MAX_DLEN)
+		return false;
+
+	/* Reject payload words that happen to sit at a header boundary */
+	if (!loongson_canfd_hdr_plausible(d->hdr0, d->hdr1))
+		return false;
+
+	return true;
+}
+
+/**
+ * loongson_canfd_rx_deliver() - Build and deliver the skb for one frame
+ * @ndev: Network device
+ * @d: Validated frame descriptor
+ *
+ * Return: 1 on success, -ENOMEM if skb allocation failed.
+ */
+static int loongson_canfd_rx_deliver(struct net_device *ndev,
+				     const struct loongson_canfd_rx_desc *d)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	struct canfd_frame *cfd;
+	struct can_frame *ccf;
+	struct sk_buff *skb;
+	u32 dlc, i;
+
+	if (d->is_can_fd) {
+		skb = alloc_canfd_skb(ndev, &cfd);
+	} else {
+		skb = alloc_can_skb(ndev, &ccf);
+		cfd = (struct canfd_frame *)ccf;
+	}
+
+	if (unlikely(!skb))
+		return -ENOMEM;
+
+	if (d->hdr0 & REG_FRAME_META0_XDT)
+		cfd->can_id = (d->hdr0 & CAN_EFF_MASK) | CAN_EFF_FLAG;
+	else
+		cfd->can_id = FIELD_GET(REG_FRAME_META0_ID_BASE, d->hdr0) &
+			      CAN_SFF_MASK;
+
+	cfd->flags = 0;
+
+	if (d->is_can_fd) {
+		if (d->hdr1 & REG_FRAME_META1_BRS)
+			cfd->flags |= CANFD_BRS;
+		if (d->hdr0 & REG_FRAME_META0_ESI)
+			cfd->flags |= CANFD_ESI;
+	} else if (d->is_rtr) {
+		cfd->can_id |= CAN_RTR_FLAG;
+	}
+
+	dlc = FIELD_GET(REG_FRAME_META1_DLC, d->hdr1);
+	if (d->is_can_fd)
+		cfd->len = can_fd_dlc2len(dlc);
+	else
+		can_frame_set_cc_len(ccf, dlc, priv->can.ctrlmode);
+
+	/*
+	 * dbcnt is authoritative. Clamp len to it so uninitialized skb
+	 * bytes are never exposed; RTR frames carry no payload and are
+	 * the exception.
+	 */
+	if (!d->is_rtr && unlikely(cfd->len > d->dbcnt))
+		cfd->len = d->dbcnt;
+
+	for (i = 0; i < d->dbcnt; i += LOONGSON_CANFD_DW_BYTE)
+		put_unaligned_le32(loongson_canfd_read_rx_word_fifo(priv),
+				   cfd->data + i);
+
+	if (!d->is_rtr)
+		stats->rx_bytes += cfd->len;
+	stats->rx_packets++;
+
+	/* Raw 0 is a legal counter sample on the wrap boundary */
+	if (READ_ONCE(priv->hwts.ready)) {
+		u32 raw = FIELD_GET(REG_FRAME_META1_TIMESTAMP, d->hdr1);
+
+		skb_hwtstamps(skb)->hwtstamp = loongson_canfd_hwts_ts(priv, raw);
+	}
+
+	netif_receive_skb(skb);
+	return 1;
+}
+
+/**
+ * loongson_canfd_rx_one_frame_fifo() - Process one RX step in FIFO mode
+ * @ndev: Network device
+ *
+ * Reads one frame header, validates it, and delivers the frame.
+ * Malformed frames are dropped and accounted internally.
+ *
+ * Return: 1 - one frame consumed (delivered or dropped on purpose)
+ */
+static int loongson_canfd_rx_one_frame_fifo(struct net_device *ndev)
+{
+	struct loongson_canfd_rx_desc d;
+	int ret;
+
+	if (!loongson_canfd_rx_hdr_valid(ndev, &d)) {
+		loongson_canfd_rx_note_bad(ndev, d.rwcnt);
+		return 1;
+	}
+
+	ret = loongson_canfd_rx_deliver(ndev, &d);
+	if (ret == -ENOMEM)
+		loongson_canfd_rx_note_bad(ndev, d.rwcnt);
+
+	return 1;
+}
+
+/**
+ * loongson_canfd_rx_overrun_event() - Account and clear a hardware RX overrun
+ * @ndev: Network device
+ *
+ * Called from hard IRQ context on INT_STAT.DOI, so the error frame is
+ * queued via netif_rx() and logging is rate-limited. DOR is cleared
+ * with REG_CMD_CDO; if the FIFO is still full it re-latches and DOI
+ * fires again.
+ *
+ * Return: true if DOR was latched and has been cleared, false if
+ *         this was a stale DOI (no live DOR).
+ */
+static bool loongson_canfd_rx_overrun_event(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	struct can_frame *cf;
+	struct sk_buff *skb;
+	u32 stat;
+
+	/* DOR is the authoritative sticky flag, DOI is only a hint */
+	regmap_read(priv->regmap, LOONGSON_CANFD_STAT, &stat);
+	if (!(stat & REG_STAT_DOR))
+		return false;
+
+	scoped_guard(spinlock_irqsave, &priv->stats_lock) {
+		stats->rx_over_errors++;
+		stats->rx_errors++;
+	}
+
+	if (net_ratelimit())
+		netdev_err(ndev, "Rx Overflow\n");
+
+	/*
+	 * Overrun is a controller condition, not a bus error: report it
+	 * as CAN_ERR_CRTL/CAN_ERR_CRTL_RX_OVERFLOW. A failed allocation
+	 * only loses the notification.
+	 */
+	skb = alloc_can_err_skb(ndev, &cf);
+	if (skb) {
+		cf->can_id |= CAN_ERR_CRTL;
+		cf->data[1] = CAN_ERR_CRTL_RX_OVERFLOW;
+
+		scoped_guard(spinlock_irqsave, &priv->stats_lock) {
+			stats->rx_packets++;
+			stats->rx_bytes += cf->len;
+		}
+
+		/* Hard IRQ: use netif_rx(), not netif_receive_skb() */
+		netif_rx(skb);
+	}
+
+	/*
+	 * Clear the sticky DOR first. The caller clears DOI right after
+	 * this returns; if DOR were still latched at that point, the
+	 * hardware would immediately re-raise DOI.
+	 */
+	regmap_write(priv->regmap, LOONGSON_CANFD_CMD, REG_CMD_CDO);
+
+	return true;
+}
+
+/**
+ * loongson_canfd_rx_napi_done() - Complete NAPI and re-arm the RX interrupt
+ * @napi: NAPI instance
+ * @can_complete: True if the poll loop saw an empty hardware FIFO
+ * @work_done: Frames processed during this poll
+ *
+ * The RX interrupt is re-armed only after the loop actually quiesced;
+ * a quota-limited exit keeps NAPI scheduled to poll again.
+ */
+static void loongson_canfd_rx_napi_done(struct napi_struct *napi,
+					bool can_complete, int work_done)
+{
+	struct net_device *ndev = napi->dev;
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	if (!can_complete || !napi_complete_done(napi, work_done))
+		return;
+
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+		     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_RBNEI));
+}
+
+/**
+ * loongson_canfd_rx_napi() - NAPI poll routine for RX
+ * @napi: NAPI structure
+ * @quota: Maximum number of frames to process
+ *
+ * Return: Number of frames processed.
+ */
+static int loongson_canfd_rx_napi(struct napi_struct *napi, int quota)
+{
+	struct net_device *ndev = napi->dev;
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	bool can_complete = false;
+	int work_done = 0;
+	int ret;
+
+	while (work_done < quota) {
+		/* FIFO empty: nothing left to consume */
+		if (loongson_canfd_rx_is_empty_fifo(priv)) {
+			can_complete = true;
+			break;
+		}
+
+		ret = loongson_canfd_rx_one_frame_fifo(ndev);
+		if (ret == 1)
+			work_done++;
+	}
+
+	loongson_canfd_rx_napi_done(napi, can_complete, work_done);
+
+	return work_done;
+}
+
+/**
+ * loongson_canfd_get_bec() - Read error counters
+ * @priv: Driver private data
+ * @bec: Structure to fill with TX/RX error counts
+ */
+static void loongson_canfd_get_bec(struct loongson_canfd_priv *priv,
+				   struct can_berr_counter *bec)
+{
+	u32 erc;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_ERC, &erc);
+	bec->rxerr = FIELD_GET(REG_ERC_REC, erc);
+	bec->txerr = FIELD_GET(REG_ERC_TEC, erc);
+}
+
+/**
+ * loongson_canfd_get_berr_counter() - Get error counter (callback)
+ * @ndev: Network device
+ * @bec: Structure to fill
+ *
+ * Return: 0 always.
+ */
+static int loongson_canfd_get_berr_counter(const struct net_device *ndev,
+					   struct can_berr_counter *bec)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	loongson_canfd_get_bec(priv, bec);
+	return 0;
+}
+
+/**
+ * loongson_canfd_get_fault_state() - Read fault confinement state
+ * @ndev: Network device
+ *
+ * Return: Current CAN state (ACTIVE, WARNING, PASSIVE, BUS_OFF).
+ */
+static enum can_state loongson_canfd_get_fault_state(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	enum can_state sts = CAN_STATE_ERROR_PASSIVE;
+	u32 fstat, ewl, erl, rec_tec, max_tec;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_FSTAT, &fstat);
+
+	switch (FIELD_GET(REG_FSTAT_MASK, fstat)) {
+	case REG_FSTAT_ERA:
+		regmap_read(priv->regmap, LOONGSON_CANFD_ERL, &erl);
+		regmap_read(priv->regmap, LOONGSON_CANFD_ERC, &rec_tec);
+
+		ewl = FIELD_GET(REG_ERL_EW, erl);
+		max_tec = max(FIELD_GET(REG_ERC_REC, rec_tec),
+			      FIELD_GET(REG_ERC_TEC, rec_tec));
+
+		if (ewl > max_tec)
+			sts = CAN_STATE_ERROR_ACTIVE;
+		else
+			sts = CAN_STATE_ERROR_WARNING;
+		break;
+	case REG_FSTAT_ERP:
+		sts = CAN_STATE_ERROR_PASSIVE;
+		break;
+	case REG_FSTAT_BOF:
+		sts = CAN_STATE_BUS_OFF;
+		break;
+	default:
+		netdev_err(ndev, "Invalid error state\n");
+		break;
+	}
+
+	return sts;
+}
+
+/**
+ * loongson_canfd_report_state_change() - Report a CAN state transition
+ * @ndev: Network device
+ * @new_state: New CAN state reported by the controller
+ * @bec: Current error counters (used to fill the error frame)
+ *
+ * Called only when the protocol state actually moved: the controller
+ * latches FCSI on reset-to-Error-Active even without a visible
+ * transition, and the caller filters that case.
+ */
+static void loongson_canfd_report_state_change(struct net_device *ndev,
+					       enum can_state new_state,
+					       const struct can_berr_counter *bec)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct can_frame *cf;
+	struct sk_buff *skb;
+
+	netdev_info(ndev, "state changes from %s to %s\n",
+		    can_get_state_str(priv->can.state),
+		    can_get_state_str(new_state));
+
+	priv->can.state = new_state;
+
+	/* Stats and BUS_OFF handling must not depend on skb allocation */
+	switch (new_state) {
+	case CAN_STATE_BUS_OFF:
+		priv->can.can_stats.bus_off++;
+		can_bus_off(ndev);
+		break;
+	case CAN_STATE_ERROR_PASSIVE:
+		priv->can.can_stats.error_passive++;
+		break;
+	case CAN_STATE_ERROR_WARNING:
+		priv->can.can_stats.error_warning++;
+		break;
+	default:
+		break;
+	}
+
+	skb = alloc_can_err_skb(ndev, &cf);
+	if (!skb)
+		return;
+
+	switch (new_state) {
+	case CAN_STATE_BUS_OFF:
+		cf->can_id |= CAN_ERR_BUSOFF;
+		goto send;
+	case CAN_STATE_ERROR_ACTIVE:
+		cf->can_id |= CAN_ERR_CNT;
+		cf->data[1] = CAN_ERR_CRTL_ACTIVE;
+		break;
+	case CAN_STATE_ERROR_PASSIVE:
+		cf->can_id |= CAN_ERR_CRTL | CAN_ERR_CNT;
+		if (bec->rxerr >= CAN_ERROR_PASSIVE_THRESHOLD)
+			cf->data[1] = CAN_ERR_CRTL_RX_PASSIVE;
+		else
+			cf->data[1] = CAN_ERR_CRTL_TX_PASSIVE;
+		break;
+	case CAN_STATE_ERROR_WARNING:
+		cf->can_id |= CAN_ERR_CRTL | CAN_ERR_CNT;
+		if (bec->txerr > bec->rxerr)
+			cf->data[1] |= CAN_ERR_CRTL_TX_WARNING;
+		else
+			cf->data[1] |= CAN_ERR_CRTL_RX_WARNING;
+		break;
+	default:
+		netdev_err(ndev, "Unexpected state: %d\n", new_state);
+		kfree_skb(skb);
+		return;
+	}
+
+	cf->data[6] = bec->txerr;
+	cf->data[7] = bec->rxerr;
+send:
+	netif_rx(skb);
+}
+
+/**
+ * loongson_canfd_report_arb_lost() - Report an arbitration lost event
+ * @ndev: Network device
+ *
+ * Accounts the event and emits the corresponding CAN error frame.
+ */
+static void loongson_canfd_report_arb_lost(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct can_frame *cf;
+	struct sk_buff *skb;
+
+	priv->can.can_stats.arbitration_lost++;
+
+	skb = alloc_can_err_skb(ndev, &cf);
+	if (!skb)
+		return;
+
+	cf->can_id |= CAN_ERR_LOSTARB;
+	cf->data[0] = CAN_ERR_LOSTARB_UNSPEC;
+
+	netif_rx(skb);
+}
+
+/**
+ * loongson_canfd_report_bus_error() - Report a bus error event
+ * @ndev: Network device
+ *
+ * Accounts the event and emits the corresponding CAN error frame.
+ */
+static void loongson_canfd_report_bus_error(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	struct can_frame *cf;
+	struct sk_buff *skb;
+
+	priv->can.can_stats.bus_error++;
+	scoped_guard(spinlock_irqsave, &priv->stats_lock) {
+		stats->rx_errors++;
+	}
+
+	skb = alloc_can_err_skb(ndev, &cf);
+	if (!skb)
+		return;
+
+	cf->can_id |= CAN_ERR_PROT | CAN_ERR_BUSERROR;
+	cf->data[2] = CAN_ERR_PROT_UNSPEC;
+	cf->data[3] = CAN_ERR_PROT_LOC_UNSPEC;
+
+	netif_rx(skb);
+}
+
+/**
+ * loongson_canfd_err_interrupt() - Handle error interrupts
+ * @ndev: Network device
+ * @isr: Interrupt status register value
+ *
+ * Dispatches error events to their respective handlers and re-enables
+ * the serviced interrupt sources.
+ */
+static void loongson_canfd_err_interrupt(struct net_device *ndev, u32 isr)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct can_berr_counter bec;
+	u32 capt, alc, mask = 0;
+	enum can_state state;
+
+	loongson_canfd_get_bec(priv, &bec);
+	regmap_read(priv->regmap, LOONGSON_CANFD_ERR_CAPT, &capt);
+	regmap_read(priv->regmap, LOONGSON_CANFD_ALC, &alc);
+
+	if (net_ratelimit())
+		netdev_info(ndev, "%s: ISR 0x%08x, rxerr %d, txerr %d, error type %lu, pos %lu, ALC id_field %lu, bit %lu\n",
+			    __func__, isr, bec.rxerr, bec.txerr,
+			    FIELD_GET(REG_ERR_CAPT_TYPE, capt),
+			    FIELD_GET(REG_ERR_CAPT_POS, capt),
+			    FIELD_GET(REG_ALC_ID_FIELD, alc),
+			    FIELD_GET(REG_ALC_BIT_POS, alc));
+
+	if (isr & (REG_INT_STAT_FCSI | REG_INT_STAT_EWLI)) {
+		mask |= REG_INT_STAT_FCSI | REG_INT_STAT_EWLI;
+		state = loongson_canfd_get_fault_state(ndev);
+
+		/*
+		 * FCSI is latched on reset -> Error Active, but that is
+		 * not a protocol-visible transition: filter it out.
+		 */
+		if (priv->can.state != state)
+			loongson_canfd_report_state_change(ndev, state, &bec);
+	}
+
+	if (isr & REG_INT_STAT_ALI) {
+		mask |= REG_INT_STAT_ALI;
+		netdev_dbg(ndev, "Arbitration lost interrupt\n");
+		loongson_canfd_report_arb_lost(ndev);
+	}
+
+	if (isr & REG_INT_STAT_BEI) {
+		mask |= REG_INT_STAT_BEI;
+		netdev_dbg(ndev, "Bus error interrupt\n");
+		loongson_canfd_report_bus_error(ndev);
+	}
+
+	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+		     FIELD_PREP(REG_INT_MASK_CLR, mask));
+}
+
+/**
+ * loongson_canfd_tx_interrupt() - Handle TX completion interrupt
+ * @ndev: Network device
+ *
+ * For each completed TX buffer: TX_BS_VALID echoes the skb (with an
+ * optional HW TX timestamp) and counts it sent; TX_BS_FAIL and
+ * TX_BS_CANCEL free the skb as dropped. Clears the completion record
+ * per slot and wakes the queue if a buffer became free.
+ */
+static void loongson_canfd_tx_interrupt(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	enum loongson_canfd_txbuf_sts sts;
+	u32 tx_frame_cnt, tx_stat;
+	bool processed = false;
+	struct sk_buff *skb;
+	bool wake;
+
+	/*
+	 * Hold tx_lock only for the buffer scan and bookkeeping;
+	 * netif_wake_queue() is called outside so a re-entrant
+	 * loongson_canfd_start_xmit() on another CPU is not blocked
+	 * on this lock.
+	 */
+	scoped_guard(spinlock_irqsave, &priv->tx_lock) {
+		regmap_read(priv->regmap, LOONGSON_CANFD_TX_STAT, &tx_stat);
+
+		for (unsigned int buf_id = 0; buf_id < LOONGSON_CANFD_TXBUF_NUM; buf_id++) {
+			sts = loongson_canfd_get_txbuf_sts(priv, buf_id);
+
+			if (sts == TX_BS_IDLE) {
+				/* BRP set: hardware still queued/arbitrating */
+				if (tx_stat & BIT(buf_id))
+					continue;
+
+				/*
+				 * Hardware is idle but echo_skb is occupied
+				 * (lost interrupt). Free it, but do not issue
+				 * SR_CLEAN on an idle slot: it can fake a
+				 * command interrupt and cause a storm.
+				 */
+				if (priv->can.echo_skb[buf_id]) {
+					netdev_warn(ndev,
+						    "TX buf %u idle but echo_skb occupied, force free\n",
+						    buf_id);
+					can_free_echo_skb(ndev, buf_id, NULL);
+					stats->tx_dropped++;
+				}
+				continue;
+			}
+
+			processed = true;
+			switch (sts) {
+			case TX_BS_VALID:
+				u32 dummy_len;
+
+				/*
+				 * The controller does not latch a per-frame
+				 * TX stamp, so the instant the handler reads
+				 * the counter is the best estimate of the
+				 * end of the frame.
+				 */
+				skb = priv->can.echo_skb[buf_id];
+				if (skb &&
+				    (skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS) &&
+				    READ_ONCE(priv->hwts.ready))
+					skb_hwtstamps(skb)->hwtstamp =
+							loongson_canfd_hwts_now(priv);
+
+				dummy_len = can_get_echo_skb(ndev, buf_id, NULL);
+				(void)dummy_len;
+				stats->tx_packets++;
+				break;
+			case TX_BS_FAIL:
+				regmap_read(priv->regmap, LOONGSON_CANFD_TX_FR_CNT,
+					    &tx_frame_cnt);
+				netdev_dbg(ndev, "TX buffer in FAIL state, TX frame count: %d\n",
+					   tx_frame_cnt);
+				can_free_echo_skb(ndev, buf_id, NULL);
+				stats->tx_dropped++;
+				break;
+			case TX_BS_CANCEL:
+				netdev_dbg(ndev, "TX buffer in CANCEL state\n");
+				can_free_echo_skb(ndev, buf_id, NULL);
+				stats->tx_dropped++;
+				break;
+			default:
+				netdev_dbg(ndev, "Unexpected TX status: %d\n", sts);
+				break;
+			}
+
+			/* Clear the completion record only for finished slots */
+			if (loongson_canfd_set_txbuf_cmd(ndev, TXT_CMD_SR_CLEAN, buf_id))
+				netdev_warn(ndev, "Failed to clear TX buffer %u status\n", buf_id);
+		}
+
+		if (!processed)
+			netdev_warn_once(ndev, "Spurious TX interrupt\n");
+
+		wake = loongson_canfd_txbuf_freed(priv);
+	}
+
+	if (wake)
+		netif_wake_queue(ndev);
+}
+
+/**
+ * loongson_canfd_interrupt() - Main interrupt handler
+ * @irq: IRQ number
+ * @dev_id: Device identifier (net_device)
+ *
+ * Return: IRQ_HANDLED if interrupt was handled, IRQ_NONE otherwise.
+ */
+static irqreturn_t loongson_canfd_interrupt(int irq, void *dev_id)
+{
+	struct net_device *ndev = (struct net_device *)dev_id;
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 isr, imask;
+
+	for (unsigned int loops = 0; loops < LOONGSON_CANFD_IRQ_LOOP_MAX; loops++) {
+		regmap_read(priv->regmap, LOONGSON_CANFD_INT_STAT, &isr);
+		if (!isr) {
+			if (loops)
+				return IRQ_HANDLED;
+			else
+				return IRQ_NONE;
+		}
+
+		if (isr & REG_INT_STAT_RBNEI) {
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+				     REG_INT_STAT_RBNEI);
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
+				     REG_INT_STAT_RBNEI);
+
+			napi_schedule(&priv->napi);
+		}
+
+		if (isr & REG_INT_STAT_TXBHCI) {
+			/*
+			 * Mask-clear-handle-unmask: a TX completing during
+			 * handling re-latches TXBHCI and fires a fresh IRQ
+			 * after the unmask, instead of being wiped by the
+			 * W1C in tx_interrupt().
+			 */
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+				     REG_INT_STAT_TXBHCI);
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
+				     REG_INT_STAT_TXBHCI);
+			loongson_canfd_tx_interrupt(ndev);
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+				     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_TXBHCI));
+		}
+
+		if (isr & REG_INT_STAT_DOI) {
+			/* Same discipline as TXBHCI, so an overrun during
+			 * handling is counted rather than absorbed by the
+			 * W1C below.
+			 */
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+				     REG_INT_STAT_DOI);
+
+			loongson_canfd_rx_overrun_event(ndev);
+
+			/* DOR is clear now: acknowledge DOI safely */
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
+				     REG_INT_STAT_DOI);
+
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+				     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_DOI));
+		}
+
+		imask = isr & REG_INT_STAT_ERRORI;
+		if (imask) {
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, imask);
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT, imask);
+			loongson_canfd_err_interrupt(ndev, isr);
+		}
+	}
+
+	netdev_err(ndev, "Interrupt loop timeout, state: 0x%x\n", isr);
+
+	if (isr & REG_INT_STAT_TXBHCI) {
+		for (unsigned int i = 0; i < LOONGSON_CANFD_TXBUF_NUM; i++) {
+			u32 sts = loongson_canfd_get_txbuf_sts(priv, i);
+
+			netdev_err(ndev, "TX buf %d status: 0x%x\n", i, sts);
+		}
+	}
+
+	loongson_canfd_disable_interrupts(priv);
+
+	return IRQ_HANDLED;
+}
+
+/**
+ * loongson_canfd_start_xmit() - Transmit a CAN frame
+ * @skb: Socket buffer
+ * @ndev: Network device
+ *
+ * Return: NETDEV_TX_OK on success, NETDEV_TX_BUSY if queue full.
+ */
+static netdev_tx_t loongson_canfd_start_xmit(struct sk_buff *skb, struct net_device *ndev)
+{
+	struct canfd_frame *cf = (struct canfd_frame *)skb->data;
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	int buf_id, ret;
+
+	if (can_dropped_invalid_skb(ndev, skb))
+		return NETDEV_TX_OK;
+
+	guard(spinlock_irqsave)(&priv->tx_lock);
+
+	if (unlikely(!loongson_canfd_txbuf_freed(priv))) {
+		netif_stop_queue(ndev);
+		netdev_dbg(ndev, "No free TX buffer while queue was awake!\n");
+		return NETDEV_TX_BUSY;
+	}
+
+	buf_id = loongson_canfd_find_free_txbuf(priv);
+	if (buf_id < 0) {
+		netif_stop_queue(ndev);
+		netdev_dbg(ndev, "No writable TX buffer, stopping queue\n");
+		return NETDEV_TX_BUSY;
+	}
+
+	ret = loongson_canfd_insert_frame(priv, cf, buf_id, can_is_canfd_skb(skb));
+	if (ret) {
+		netdev_err(ndev, "Failed to insert frame into TX buffer: %d\n", ret);
+		dev_kfree_skb_any(skb);
+		ndev->stats.tx_dropped++;
+		return NETDEV_TX_OK;
+	}
+
+	can_put_echo_skb(skb, ndev, buf_id, 0);
+
+	if (!(cf->can_id & CAN_RTR_FLAG))
+		stats->tx_bytes += cf->len;
+
+	ret = loongson_canfd_set_txbuf_cmd(ndev, TXT_CMD_ADD, buf_id);
+	if (ret) {
+		netdev_err(ndev, "Failed to start TX buffer %d: %d\n",
+			   buf_id, ret);
+		can_free_echo_skb(ndev, buf_id, NULL);
+		ndev->stats.tx_dropped++;
+		return NETDEV_TX_OK;
+	}
+
+	if (!loongson_canfd_txbuf_freed(priv))
+		netif_stop_queue(ndev);
+
+	return NETDEV_TX_OK;
+}
+
+/**
+ * loongson_canfd_open() - Open the network device
+ * @ndev: Network device
+ *
+ * Return: 0 on success, negative error on failure.
+ */
+static int loongson_canfd_open(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	int ret;
+
+	loongson_canfd_reset(ndev);
+
+	ret = open_candev(ndev);
+	if (ret) {
+		netdev_warn(ndev, "open_candev failed\n");
+		return ret;
+	}
+
+	napi_enable(&priv->napi);
+	ret = request_irq(ndev->irq, loongson_canfd_interrupt,
+			  IRQF_SHARED, ndev->name, ndev);
+	if (ret < 0) {
+		netdev_err(ndev, "IRQ request failed\n");
+		goto exit_napi_disable;
+	}
+
+	ret = loongson_canfd_chip_start(ndev);
+	if (ret < 0) {
+		netdev_err(ndev, "Chip start failed\n");
+		goto exit_free_irq;
+	}
+
+	netdev_info(ndev, "Loongson CANFD device registered\n");
+	netif_start_queue(ndev);
+
+	return 0;
+
+exit_free_irq:
+	free_irq(ndev->irq, ndev);
+exit_napi_disable:
+	napi_disable(&priv->napi);
+	close_candev(ndev);
+	return ret;
+}
+
+/**
+ * loongson_canfd_close() - Close the network device
+ * @ndev: Network device
+ *
+ * Cancels a pending bus-off restart before tearing down IRQ/DMA, since
+ * loongson_canfd_do_set_mode() re-arms both.
+ *
+ * Return: 0 always.
+ */
+static int loongson_canfd_close(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	netif_stop_queue(ndev);
+	napi_disable(&priv->napi);
+
+	/*
+	 * Cancel a pending bus-off restart before tearing down IRQ/DMA:
+	 * do_set_mode() re-arms both, so a restart firing after
+	 * free_irq() would touch released resources. close_candev()
+	 * does the same but only after free_irq().
+	 */
+	cancel_delayed_work_sync(&priv->can.restart_work);
+
+	loongson_canfd_chip_stop(ndev);
+	free_irq(ndev->irq, ndev);
+	close_candev(ndev);
+
+	return 0;
+}
+
+static const struct net_device_ops loongson_canfd_netdev_ops = {
+	.ndo_open           = loongson_canfd_open,
+	.ndo_stop           = loongson_canfd_close,
+	.ndo_start_xmit     = loongson_canfd_start_xmit,
+	.ndo_hwtstamp_get   = can_hwtstamp_get,
+	.ndo_hwtstamp_set   = can_hwtstamp_set,
+};
+
+/**
+ * loongson_canfd_get_ts_info() - Report timestamping capabilities
+ * @ndev: Network device
+ * @info: ethtool timestamping info to fill
+ *
+ * Hardware timestamping is advertised only while @hwts.ready is set,
+ * so the reported capabilities match the driver's actual state.
+ *
+ * Return: 0 always.
+ */
+static int loongson_canfd_get_ts_info(struct net_device *ndev,
+				      struct kernel_ethtool_ts_info *info)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	info->so_timestamping = SOF_TIMESTAMPING_SOFTWARE |
+				SOF_TIMESTAMPING_TX_SOFTWARE |
+				SOF_TIMESTAMPING_RX_SOFTWARE;
+
+	info->phc_index = -1;
+
+	if (READ_ONCE(priv->hwts.ready)) {
+		info->so_timestamping |= SOF_TIMESTAMPING_TX_HARDWARE |
+					 SOF_TIMESTAMPING_RX_HARDWARE |
+					 SOF_TIMESTAMPING_RAW_HARDWARE;
+		info->tx_types = BIT(HWTSTAMP_TX_ON);
+		info->rx_filters = BIT(HWTSTAMP_FILTER_ALL);
+	}
+
+	return 0;
+}
+
+static const struct ethtool_ops loongson_canfd_ethtool_ops = {
+	.get_ts_info = loongson_canfd_get_ts_info,
+};
+
+static const struct regmap_range loongson_canfd_reg_table_wr_range[] = {
+	regmap_reg_range(LOONGSON_CANFD_DEVICE_ID, LOONGSON_CANFD_CONF),
+	regmap_reg_range(LOONGSON_CANFD_CMD, LOONGSON_CANFD_CMD),
+	regmap_reg_range(LOONGSON_CANFD_INT_STAT, LOONGSON_CANFD_ERL),
+	regmap_reg_range(LOONGSON_CANFD_CTR_PRES, LOONGSON_CANFD_CTR_PRES),
+	regmap_reg_range(LOONGSON_CANFD_SSP_CFG, LOONGSON_CANFD_SSP_CFG),
+	regmap_reg_range(LOONGSON_CANFD_TS, LOONGSON_CANFD_FLT_CTRL),
+	regmap_reg_range(LOONGSON_CANFD_TX_CMD, LOONGSON_CANFD_TX_DATA_18),
+};
+
+static const struct regmap_range loongson_canfd_reg_table_rd_range[] = {
+	regmap_reg_range(LOONGSON_CANFD_DEVICE_ID, LOONGSON_CANFD_STAT),
+	regmap_reg_range(LOONGSON_CANFD_INT_STAT, LOONGSON_CANFD_BRE),
+	regmap_reg_range(LOONGSON_CANFD_ERR_CAPT, LOONGSON_CANFD_TX_STAT),
+	regmap_reg_range(LOONGSON_CANFD_TX_SEL, LOONGSON_CANFD_TX_DATA_18),
+};
+
+static const struct regmap_access_table loongson_canfd_reg_table_wr = {
+	.yes_ranges = loongson_canfd_reg_table_wr_range,
+	.n_yes_ranges = ARRAY_SIZE(loongson_canfd_reg_table_wr_range),
+};
+
+static const struct regmap_access_table loongson_canfd_reg_table_rd = {
+	.yes_ranges = loongson_canfd_reg_table_rd_range,
+	.n_yes_ranges = ARRAY_SIZE(loongson_canfd_reg_table_rd_range),
+};
+
+static bool loongson_canfd_volatile_reg(struct device *dev, unsigned int reg)
+{
+	switch (reg) {
+	case LOONGSON_CANFD_MODE:
+	case LOONGSON_CANFD_CONF:
+	case LOONGSON_CANFD_STAT:
+	case LOONGSON_CANFD_CMD:
+	case LOONGSON_CANFD_INT_STAT:
+	case LOONGSON_CANFD_INT_ENA:
+	case LOONGSON_CANFD_INT_MASK:
+	case LOONGSON_CANFD_ERL:
+	case LOONGSON_CANFD_FSTAT:
+	case LOONGSON_CANFD_ERC:
+	case LOONGSON_CANFD_ERR_CAPT:
+	case LOONGSON_CANFD_ALC:
+	case LOONGSON_CANFD_TRV_DLY:
+	case LOONGSON_CANFD_TX_FR_CNT:
+	case LOONGSON_CANFD_TS:
+	case LOONGSON_CANFD_RX_STAT:
+	case LOONGSON_CANFD_RX_DATA:
+	case LOONGSON_CANFD_TX_STAT:
+	case LOONGSON_CANFD_TX_CMD:
+	case LOONGSON_CANFD_TX_SEL:
+		return true;
+	default:
+		return false;
+	};
+}
+
+static const struct regmap_config loongson_canfd_regmap = {
+	.reg_bits	= 32,
+	.reg_stride	= 4,
+	.val_bits	= 32,
+	.wr_table	= &loongson_canfd_reg_table_wr,
+	.rd_table	= &loongson_canfd_reg_table_rd,
+	.volatile_reg	= loongson_canfd_volatile_reg,
+	.max_register	= LOONGSON_CANFD_TX_DATA_18,
+	.cache_type	= REGCACHE_MAPLE,
+};
+
+/**
+ * loongson_canfd_setup_can_priv() - Fill in can_priv static configuration
+ * @ndev: Network device
+ * @clk_rate: Controller input clock, in Hz
+ *
+ * The whole can_priv configuration is data-only; keeping it out of
+ * probe() makes the resource-acquisition flow easier to follow.
+ */
+static void loongson_canfd_setup_can_priv(struct net_device *ndev, u32 clk_rate)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	priv->can.clock.freq = clk_rate;
+	priv->can.bittiming_const = &loongson_canfd_bit_timing;
+	priv->can.fd.data_bittiming_const = &loongson_canfd_bit_timing_data;
+	priv->can.fd.tdc_const = &loongson_canfd_tdc_data;
+
+	priv->can.fd.do_set_data_bittiming = loongson_canfd_set_data_bittiming;
+	priv->can.fd.do_get_auto_tdcv = loongson_canfd_get_auto_tdcv;
+	priv->can.do_set_mode = loongson_canfd_do_set_mode;
+	priv->can.do_set_bittiming = loongson_canfd_set_bittiming;
+	priv->can.do_get_berr_counter = loongson_canfd_get_berr_counter;
+
+	priv->can.ctrlmode_supported = CAN_CTRLMODE_LOOPBACK |
+				       CAN_CTRLMODE_LISTENONLY |
+				       CAN_CTRLMODE_ONE_SHOT |
+				       CAN_CTRLMODE_BERR_REPORTING |
+				       CAN_CTRLMODE_FD |
+				       CAN_CTRLMODE_PRESUME_ACK |
+				       CAN_CTRLMODE_FD_NON_ISO |
+				       CAN_CTRLMODE_TDC_AUTO |
+				       CAN_CTRLMODE_CC_LEN8_DLC;
+}
+
+static int loongson_canfd_probe(struct platform_device *pdev)
+{
+	struct loongson_canfd_priv *priv;
+	struct device *dev = &pdev->dev;
+	struct resource *mem_res;
+	struct net_device *ndev;
+	struct regmap *regmap;
+	void __iomem *base;
+	int ret, irq;
+	u32 clk_rate;
+
+	base = devm_platform_get_and_ioremap_resource(pdev, 0, &mem_res);
+	if (IS_ERR(base))
+		return PTR_ERR(base);
+
+	regmap = devm_regmap_init_mmio(dev, base, &loongson_canfd_regmap);
+	if (IS_ERR(regmap))
+		return PTR_ERR(regmap);
+
+	ret = device_property_read_u32(dev, "clock-frequency", &clk_rate);
+	if (ret)
+		return dev_err_probe(dev, ret, "Missing clock-frequency property");
+
+	irq = platform_get_irq(pdev, 0);
+	if (irq < 0)
+		return dev_err_probe(dev, irq, "Failed to get IRQ");
+
+	ndev = alloc_candev(sizeof(*priv), LOONGSON_CANFD_TXBUF_NUM);
+	if (!ndev)
+		return -ENOMEM;
+
+	priv = netdev_priv(ndev);
+	spin_lock_init(&priv->tx_lock);
+	spin_lock_init(&priv->stats_lock);
+	spin_lock_init(&priv->hwts.lock);
+	priv->regmap = regmap;
+	priv->mem_res = mem_res;
+
+	loongson_canfd_setup_can_priv(ndev, clk_rate);
+
+	ndev->irq = irq;
+	ndev->flags |= IFF_ECHO;
+	platform_set_drvdata(pdev, ndev);
+	ndev->netdev_ops = &loongson_canfd_netdev_ops;
+	ndev->ethtool_ops = &loongson_canfd_ethtool_ops;
+	SET_NETDEV_DEV(ndev, dev);
+
+	netif_napi_add(ndev, &priv->napi, loongson_canfd_rx_napi);
+
+	ret = register_candev(ndev);
+	if (ret) {
+		dev_err(dev, "register_candev failed with %d", ret);
+		goto err_candev_free;
+	}
+
+	return 0;
+
+err_candev_free:
+	netif_napi_del(&priv->napi);
+	free_candev(ndev);
+	return ret;
+}
+
+/**
+ * loongson_canfd_remove() - Remove the device
+ * @pdev: Platform device
+ */
+static void loongson_canfd_remove(struct platform_device *pdev)
+{
+	struct net_device *ndev = platform_get_drvdata(pdev);
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+
+	unregister_candev(ndev);
+	netif_napi_del(&priv->napi);
+	free_candev(ndev);
+}
+
+static const struct acpi_device_id loongson_canfd_acpi_match[] = {
+	{ .id = "LOON0015" },
+	{ }
+};
+MODULE_DEVICE_TABLE(acpi, loongson_canfd_acpi_match);
+
+static struct platform_driver loongson_canfd_driver = {
+	.probe  = loongson_canfd_probe,
+	.remove = loongson_canfd_remove,
+	.driver = {
+		.name = DEV_NAME,
+		.acpi_match_table = loongson_canfd_acpi_match,
+	},
+};
+module_platform_driver(loongson_canfd_driver);
+
+MODULE_AUTHOR("Loongson Technology Corporation Limited");
+MODULE_DESCRIPTION("Loongson CAN-FD Controller driver");
+MODULE_LICENSE("GPL");
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support
  2026-11-09  8:42 [PATCH v9 0/2] Add Loongson CAN-FD controller driver Binbin Zhou
  2026-11-09  8:42 ` [PATCH v9 1/2] can: " Binbin Zhou
@ 2026-11-09  8:42 ` Binbin Zhou
  2026-11-09  9:51   ` sashiko-bot
  2026-11-10 22:20   ` netdev-bot+sashiko
  1 sibling, 2 replies; 7+ messages in thread
From: Binbin Zhou @ 2026-11-09  8:42 UTC (permalink / raw)
  To: Binbin Zhou, Huacai Chen, Marc Kleine-Budde, Vincent Mailhol,
	Bingxiong Li
  Cc: Huacai Chen, Xuerui Wang, loongarch, linux-can, jeffbai,
	Binbin Zhou

Add optional RX DMA support to the Loongson CAN-FD driver.

When an "rx" DMA channel is available, incoming frames are moved into
a ring buffer by a cyclic DMA transfer and read directly from there,
reducing MMIO accesses and improving RX throughput under high traffic.

The DMA path is fully transparent to the networking stack; the driver
falls back to FIFO polling when no DMA channel is present.

Co-developed-by: Bingxiong Li <libingxiong@loongson.cn>
Signed-off-by: Bingxiong Li <libingxiong@loongson.cn>
Signed-off-by: Binbin Zhou <zhoubinbin@loongson.cn>
---
 drivers/net/can/loongson_canfd.c | 697 ++++++++++++++++++++++++++++++-
 1 file changed, 679 insertions(+), 18 deletions(-)

diff --git a/drivers/net/can/loongson_canfd.c b/drivers/net/can/loongson_canfd.c
index 7a4d8df7815a..b5a5917bc773 100644
--- a/drivers/net/can/loongson_canfd.c
+++ b/drivers/net/can/loongson_canfd.c
@@ -12,6 +12,9 @@
 #include <linux/can/error.h>
 #include <linux/cleanup.h>
 #include <linux/clocksource.h>
+#include <linux/dmaengine.h>
+#include <linux/dma-direction.h>
+#include <linux/dma-mapping.h>
 #include <linux/ethtool.h>
 #include <linux/interrupt.h>
 #include <linux/module.h>
@@ -344,8 +347,11 @@
 #define DEV_NAME			"loongson_canfd"
 #define LOONGSON_CANFD_ID		0xBABE
 #define LOONGSON_CANFD_DW_BYTE		4
+#define LOONGSON_CANFD_RXBUF_SZ		SZ_32K
+#define LOONGSON_CANFD_RXDMA_PERIOD	SZ_4K
 #define LOONGSON_CANFD_TXBUF_NUM	8
 #define LOONGSON_CANFD_MAX_RTXTH	0xf
+#define LOONGSON_CANFD_RXDMA_NUM	(LOONGSON_CANFD_RXBUF_SZ / DMA_SLAVE_BUSWIDTH_4_BYTES)
 
 /* Upper bound on the number of ISR drain iterations */
 #define LOONGSON_CANFD_IRQ_LOOP_MAX		10000
@@ -362,6 +368,9 @@
 #define LOONGSON_CANFD_HWTS_SLEW_SHIFT		3
 #define LOONGSON_CANFD_HWTS_SLEW_BAND_NS	2000
 
+/* cpu_relax() iterations waiting for the DMA to finish a peeked frame */
+#define LOONGSON_CANFD_DMA_WAIT_RETRY_MAX	100
+
 /**
  * struct loongson_canfd_hwts - Hardware timestamp state
  * @cc: Cycle counter abstracting the 16-bit timestamp counter
@@ -409,6 +418,17 @@ struct loongson_canfd_rx_desc {
  * @tx_lock: Lock for synchronizing TX interrupt handling
  * @stats_lock: Lock for protecting netdev statistics
  * @hwts: Hardware timestamp state
+ * @rx_ch: DMA receive channel, NULL in poll mode
+ * @rx_cookie: DMA receive cookie
+ * @rx_dma_addr: DMA RX buffer bus address
+ * @rx_cpu_buf: DMA RX buffer CPU address
+ * @rx_rd_pos: Software read position, 64-bit monotonic, in 32-bit words
+ * @rx_wr_pos: DMA write position, 64-bit monotonic, wrap-extended
+ * @rx_wr_off: Last raw residue-derived ring offset, 0..RXDMA_NUM-1
+ * @rx_bad_cnt: Consecutive invalid RX headers seen in DMA mode
+ * @read_rx_word: Callback for fetching one 32-bit word (DMA or poll mode)
+ * @rx_is_empty: Predicate: true if the hardware RX FIFO has no frame
+ * @process_rx: Callback that consumes one frame from the RX stream
  */
 struct loongson_canfd_priv {
 	struct can_priv		can;
@@ -418,6 +438,21 @@ struct loongson_canfd_priv {
 	spinlock_t		tx_lock;	/* protect the sending queue */
 	spinlock_t		stats_lock;	/* protect the ndev->stats */
 	struct loongson_canfd_hwts	hwts;
+
+	/* RX ring 64-bit monotonic positions, in 32-bit words */
+	struct dma_chan		*rx_ch;
+	dma_cookie_t		rx_cookie;
+	dma_addr_t		rx_dma_addr;
+	unsigned int		*rx_cpu_buf;
+
+	u64			rx_rd_pos;
+	u64			rx_wr_pos;
+	u32			rx_wr_off;
+	u32			rx_bad_cnt;
+
+	u32 (*read_rx_word)(struct loongson_canfd_priv *priv);
+	bool (*rx_is_empty)(struct loongson_canfd_priv *priv);
+	int (*process_rx)(struct net_device *ndev);
 };
 
 /**
@@ -504,6 +539,213 @@ static void loongson_canfd_disable_interrupts(struct loongson_canfd_priv *priv)
 	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, REG_INT_MASK_SET);
 }
 
+/**
+ * loongson_canfd_dma_sync_wr() - Refresh the 64-bit DMA write position
+ * @priv: Driver private data
+ *
+ * The DMA engine exposes only a residue; converting it to a raw ring
+ * offset cannot distinguish full from empty because the residue wraps
+ * back to the buffer size right after the ring is filled. Detect the
+ * wrap here and produce a monotonic 64-bit write position.
+ *
+ * Idempotent while the DMA engine makes no progress.
+ */
+static void loongson_canfd_dma_sync_wr(struct loongson_canfd_priv *priv)
+{
+	struct dma_tx_state state;
+	u32 off;
+
+	if (!priv->rx_ch)
+		return;
+
+	if (dmaengine_tx_status(priv->rx_ch, priv->rx_cookie, &state) !=
+	    DMA_IN_PROGRESS)
+		return;
+
+	if (state.residue > LOONGSON_CANFD_RXBUF_SZ)
+		state.residue = LOONGSON_CANFD_RXBUF_SZ;
+
+	off = (LOONGSON_CANFD_RXBUF_SZ - state.residue) /
+	      DMA_SLAVE_BUSWIDTH_4_BYTES;
+	off %= LOONGSON_CANFD_RXDMA_NUM;
+
+	if (off < priv->rx_wr_off)
+		/* The DMA engine wrapped around the end of the ring. */
+		priv->rx_wr_pos += LOONGSON_CANFD_RXDMA_NUM -
+				   priv->rx_wr_off + off;
+	else
+		priv->rx_wr_pos += off - priv->rx_wr_off;
+
+	priv->rx_wr_off = off;
+}
+
+/**
+ * loongson_canfd_read_rx_word_dma() - Read one word from the DMA buffer
+ * @priv: Driver private data
+ *
+ * Consumes the next word from the cyclic ring and advances rx_rd_pos.
+ *
+ * Return: The next 32-bit data word.
+ */
+static u32 loongson_canfd_read_rx_word_dma(struct loongson_canfd_priv *priv)
+{
+	u32 idx = priv->rx_rd_pos % LOONGSON_CANFD_RXDMA_NUM;
+
+	priv->rx_rd_pos++;
+	return priv->rx_cpu_buf[idx];
+}
+
+/**
+ * loongson_canfd_ring_peek() - Peek one word from the DMA ring
+ * @priv: Driver private data
+ * @pos: 64-bit monotonic word position
+ *
+ * Return: word at @pos without advancing rx_rd_pos.
+ */
+static u32 loongson_canfd_ring_peek(struct loongson_canfd_priv *priv, u64 pos)
+{
+	u32 idx = pos % LOONGSON_CANFD_RXDMA_NUM;
+
+	return priv->rx_cpu_buf[idx];
+}
+
+static void loongson_canfd_rxdma_free(struct loongson_canfd_priv *priv,
+				      struct device *dev)
+{
+	if (!priv->rx_cpu_buf)
+		return;
+
+	dma_free_coherent(dev, LOONGSON_CANFD_RXBUF_SZ, priv->rx_cpu_buf,
+			  priv->rx_dma_addr);
+	priv->rx_cpu_buf = NULL;
+}
+
+static void loongson_canfd_rxdma_remove(struct loongson_canfd_priv *priv,
+					struct device *dev)
+{
+	if (!priv->rx_ch)
+		return;
+
+	dmaengine_terminate_sync(priv->rx_ch);
+	loongson_canfd_rxdma_free(priv, dev);
+	dma_release_channel(priv->rx_ch);
+	priv->rx_ch = NULL;
+}
+
+/**
+ * loongson_canfd_rxdma_init() - Initialize RX DMA resources
+ * @ndev: Network device
+ *
+ * Allocates the coherent buffer and programs the slave; the channel is
+ * started later in open() so it is synchronized with the controller.
+ *
+ * On failure the channel is released and @priv->rx_ch is set to NULL;
+ * the caller must not free it again.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int loongson_canfd_rxdma_init(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct device *dev = ndev->dev.parent;
+	struct dma_slave_config config;
+	int ret;
+
+	if (!priv->rx_ch)
+		return -ENODEV;
+
+	priv->rx_cpu_buf = dma_alloc_coherent(dev, LOONGSON_CANFD_RXBUF_SZ,
+					      &priv->rx_dma_addr, GFP_KERNEL);
+	if (!priv->rx_cpu_buf) {
+		dma_release_channel(priv->rx_ch);
+		priv->rx_ch = NULL;
+		return -ENOMEM;
+	}
+
+	memset(&config, 0, sizeof(config));
+	config.src_addr = priv->mem_res->start + LOONGSON_CANFD_RX_DATA;
+	config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
+
+	ret = dmaengine_slave_config(priv->rx_ch, &config);
+	if (ret < 0) {
+		netdev_err(ndev, "RX DMA channel config failed\n");
+		goto err_config;
+	}
+
+	/* Do not start DMA in probe; it is started in open() */
+	return 0;
+
+err_config:
+	loongson_canfd_rxdma_free(priv, dev);
+	dma_release_channel(priv->rx_ch);
+	priv->rx_ch = NULL;
+	return ret;
+}
+
+/**
+ * loongson_canfd_rxdma_start() - Submit the cyclic DMA and run it continuously
+ * @ndev: Network device
+ *
+ * Prepares and submits a cyclic descriptor and lets the channel run
+ * from the first issue_pending() to rxdma_stop(). No pause/resume is
+ * used: the engine is expected to stall on its own when RX_DATA is
+ * empty. Zero-fill produced before chip_start() runs is skipped
+ * because the controller is not yet enabled.
+ *
+ * Return: 0 on success, negative error code on failure.
+ */
+static int loongson_canfd_rxdma_start(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct dma_async_tx_descriptor *desc;
+
+	if (!priv->rx_ch)
+		return 0;
+
+	priv->rx_rd_pos = 0;
+	priv->rx_wr_pos = 0;
+	priv->rx_wr_off = 0;
+	priv->rx_bad_cnt = 0;
+
+	desc = dmaengine_prep_dma_cyclic(priv->rx_ch, priv->rx_dma_addr,
+					 LOONGSON_CANFD_RXBUF_SZ,
+					 LOONGSON_CANFD_RXDMA_PERIOD,
+					 DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
+	if (!desc) {
+		netdev_err(ndev, "RX DMA cyclic prep failed\n");
+		return -EBUSY;
+	}
+
+	priv->rx_cookie = dmaengine_submit(desc);
+	if (dma_submit_error(priv->rx_cookie)) {
+		netdev_err(ndev, "RX DMA submit failed\n");
+		return -EIO;
+	}
+
+	dma_async_issue_pending(priv->rx_ch);
+
+	/* Skip zero-fill produced before the controller is enabled */
+	loongson_canfd_dma_sync_wr(priv);
+	priv->rx_rd_pos = priv->rx_wr_pos;
+
+	return 0;
+}
+
+/**
+ * loongson_canfd_rxdma_stop() - Stop the RX DMA cyclic transfer
+ * @priv: Driver private data
+ *
+ * Synchronously terminates the DMA. Positions are reset on the next
+ * rxdma_start().
+ */
+static void loongson_canfd_rxdma_stop(struct loongson_canfd_priv *priv)
+{
+	if (!priv->rx_ch)
+		return;
+
+	dmaengine_terminate_sync(priv->rx_ch);
+}
+
 /**
  * loongson_canfd_read_rx_word_fifo() - Read RX data in poll mode
  * @priv: Driver private data
@@ -922,7 +1164,12 @@ static void loongson_canfd_apply_ctrlmode(struct loongson_canfd_priv *priv)
  * loongson_canfd_chip_start() - Start the CAN controller
  * @ndev: Network device
  *
- * Configures bit timing, SSP, interrupt enables, and enables the controller.
+ * Interrupt sources used by the RX path:
+ *   RBNEI  Primary wake-up on FIFO empty -> non-empty edge; the only
+ *          source that fires on a quiet bus.
+ *   DMADI  Secondary wake-up once per cyclic period while the engine
+ *          is moving data; enabled only when a DMA channel exists.
+ *   DOI    Safety net when the hardware FIFO overflows.
  *
  * Return: 0 on success, negative error on failure.
  */
@@ -947,6 +1194,9 @@ static int loongson_canfd_chip_start(struct net_device *ndev)
 		  REG_INT_STAT_EWLI  | REG_INT_STAT_FCSI   |
 		  REG_INT_STAT_DOI;
 
+	if (priv->rx_ch)
+		int_ena |= REG_INT_STAT_DMADI;
+
 	if (priv->can.ctrlmode & CAN_CTRLMODE_BERR_REPORTING)
 		int_ena |= REG_INT_STAT_ALI | REG_INT_STAT_BEI;
 
@@ -995,15 +1245,29 @@ static void loongson_canfd_chip_stop(struct net_device *ndev)
  */
 static int loongson_canfd_do_set_mode(struct net_device *ndev, enum can_mode mode)
 {
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
 	int ret;
 
 	switch (mode) {
 	case CAN_MODE_START:
 		loongson_canfd_reset(ndev);
 
+		/*
+		 * The FIFO starts from scratch; DMA and CPU read pointer
+		 * must be realigned with the new FIFO start.
+		 */
+		loongson_canfd_rxdma_stop(priv);
+
+		ret = loongson_canfd_rxdma_start(ndev);
+		if (ret) {
+			netdev_err(ndev, "RX DMA restart failed\n");
+			return ret;
+		}
+
 		ret = loongson_canfd_chip_start(ndev);
 		if (ret < 0) {
 			netdev_err(ndev, "Failed to start chip\n");
+			loongson_canfd_rxdma_stop(priv);
 			return ret;
 		}
 
@@ -1307,7 +1571,7 @@ static bool loongson_canfd_hdr_plausible(u32 hdr0, u32 hdr1)
 static void loongson_canfd_rx_note_bad(struct net_device *ndev, u32 rwcnt)
 {
 	struct loongson_canfd_priv *priv = netdev_priv(ndev);
-	u32 words, data;
+	u32 words;
 
 	ndev->stats.rx_dropped++;
 
@@ -1315,7 +1579,7 @@ static void loongson_canfd_rx_note_bad(struct net_device *ndev, u32 rwcnt)
 		min_t(u32, rwcnt - 2,
 		      CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE) : 0;
 	while (words--)
-		regmap_read(priv->regmap, LOONGSON_CANFD_RX_DATA, &data);
+		priv->read_rx_word(priv);
 }
 
 /**
@@ -1333,8 +1597,8 @@ static bool loongson_canfd_rx_hdr_valid(struct net_device *ndev,
 {
 	struct loongson_canfd_priv *priv = netdev_priv(ndev);
 
-	d->hdr0 = loongson_canfd_read_rx_word_fifo(priv);
-	d->hdr1 = loongson_canfd_read_rx_word_fifo(priv);
+	d->hdr0 = priv->read_rx_word(priv);
+	d->hdr1 = priv->read_rx_word(priv);
 
 	d->rwcnt = FIELD_GET(REG_FRAME_META1_RWCNT, d->hdr1);
 	if (d->rwcnt < 2) {
@@ -1420,8 +1684,7 @@ static int loongson_canfd_rx_deliver(struct net_device *ndev,
 		cfd->len = d->dbcnt;
 
 	for (i = 0; i < d->dbcnt; i += LOONGSON_CANFD_DW_BYTE)
-		put_unaligned_le32(loongson_canfd_read_rx_word_fifo(priv),
-				   cfd->data + i);
+		put_unaligned_le32(priv->read_rx_word(priv), cfd->data + i);
 
 	if (!d->is_rtr)
 		stats->rx_bytes += cfd->len;
@@ -1435,6 +1698,247 @@ static int loongson_canfd_rx_deliver(struct net_device *ndev,
 	}
 
 	netif_receive_skb(skb);
+	return 0;
+}
+
+/**
+ * loongson_canfd_rx_is_empty_dma() - DMA mode: nothing left to process
+ * @priv: Driver private data
+ *
+ * The engine stalls on an empty RX_DATA, so the ring holds exactly the
+ * words the controller produced. Two defensive cases are handled: a
+ * zero header pair (DMA zero-fill) and a non-zero implausible pair
+ * (mid-frame garbage). Both are dropped silently by realigning the
+ * reader on the write pointer.
+ *
+ * A partially-resident header pair is NOT discarded: returning false
+ * lets process_rx() return -EAGAIN so NAPI retries once the second
+ * word lands.
+ *
+ * Return: true if no complete frame is left to process.
+ */
+static bool loongson_canfd_rx_is_empty_dma(struct loongson_canfd_priv *priv)
+{
+	u32 rx_stat;
+	u32 idx, h0, h1;
+
+	loongson_canfd_dma_sync_wr(priv);
+
+	if (priv->rx_wr_pos < priv->rx_rd_pos)
+		goto discard;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
+
+	/* A complete frame is waiting in the FIFO */
+	if (FIELD_GET(REG_RX_STAT_RXFRC, rx_stat))
+		return false;
+
+	/* FIFO holds a partial frame the DMA engine is still moving */
+	if (!(rx_stat & REG_RX_STAT_RXE))
+		return false;
+
+	/* Header pair not yet fully resident: let NAPI retry */
+	if (priv->rx_wr_pos < priv->rx_rd_pos + 2)
+		return false;
+
+	idx = priv->rx_rd_pos % LOONGSON_CANFD_RXDMA_NUM;
+	h0 = priv->rx_cpu_buf[idx];
+	h1 = priv->rx_cpu_buf[(idx + 1) % LOONGSON_CANFD_RXDMA_NUM];
+
+	/* Zero header: DMA zero-fill. Drop silently. */
+	if (h0 == 0 && h1 == 0)
+		goto discard;
+
+	/* Non-zero but implausible: mid-frame garbage. Drop silently. */
+	if (!loongson_canfd_hdr_plausible(h0, h1))
+		goto discard;
+
+	return false;
+
+discard:
+	priv->rx_rd_pos = priv->rx_wr_pos;
+	return true;
+}
+
+/**
+ * loongson_canfd_rx_one_frame_dma_overrun() - Recover from a DMA ring overrun
+ * @ndev: Network device
+ * @avail: Words currently pending, i.e. rx_wr_pos - rx_rd_pos
+ *
+ * The engine overwrote unread data. Resync on the oldest still-valid
+ * word instead of discarding the whole ring.
+ *
+ * Return: 0, meaning "resynced, nothing consumed".
+ */
+static int loongson_canfd_rx_one_frame_dma_overrun(struct net_device *ndev,
+						   u64 avail)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	struct dma_tx_state dbg_state;
+	u32 dbg_rx_stat = 0;
+
+	dmaengine_tx_status(priv->rx_ch, priv->rx_cookie, &dbg_state);
+	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &dbg_rx_stat);
+
+	netdev_warn(ndev, "RX DMA overrun, resyncing\n");
+	scoped_guard(spinlock_irqsave, &priv->stats_lock) {
+		stats->rx_errors++;
+		stats->rx_over_errors++;
+		stats->rx_dropped++;
+	}
+
+	/* Words older than rx_wr_pos - RXDMA_NUM are already overwritten */
+	if (priv->rx_wr_pos > LOONGSON_CANFD_RXDMA_NUM - 2)
+		priv->rx_rd_pos = priv->rx_wr_pos - LOONGSON_CANFD_RXDMA_NUM + 2;
+	else
+		priv->rx_rd_pos = 0;
+
+	priv->rx_bad_cnt = 0;
+	return 0;
+}
+
+/**
+ * loongson_canfd_rx_one_frame_dma_bad_hdr() - Recover from an invalid header
+ * @ndev: Network device
+ * @d: Descriptor containing the bad hdr0/hdr1/rwcnt
+ * @saved_pos: rx_rd_pos before the two header words were read
+ *
+ * Zero headers indicate DMA zero-fill and are dropped silently by
+ * realigning the reader on the write pointer. Non-zero implausible
+ * headers mean the reader landed mid-frame; walk forward one word at
+ * a time looking for the next plausible header, bounded to 32 words.
+ *
+ * Return:
+ *   1        - one word discarded, keep polling
+ *   -ENODATA - pointers realigned on the write pointer, NAPI may stop
+ */
+static int loongson_canfd_rx_one_frame_dma_bad_hdr(struct net_device *ndev,
+						   const struct loongson_canfd_rx_desc *d,
+						   u64 saved_pos)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	u32 rx_stat, tries;
+	u64 pos;
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
+
+	/* Zero header: DMA zero-fill. Realign on the write pointer. */
+	if (d->hdr0 == 0 && d->hdr1 == 0) {
+		loongson_canfd_dma_sync_wr(priv);
+		priv->rx_rd_pos = priv->rx_wr_pos;
+		priv->rx_bad_cnt = 0;
+		return -ENODATA;
+	}
+
+	/* Non-zero implausible header: scan forward for a plausible one */
+	stats->rx_dropped++;
+
+	for (tries = 0, pos = saved_pos + 1;
+	     tries < 32 && pos + 1 < priv->rx_wr_pos;
+	     tries++, pos++) {
+		u32 h0 = loongson_canfd_ring_peek(priv, pos);
+		u32 h1 = loongson_canfd_ring_peek(priv, pos + 1);
+
+		/* Zero-fill tail */
+		if (h0 == 0 && h1 == 0) {
+			loongson_canfd_dma_sync_wr(priv);
+			priv->rx_rd_pos = priv->rx_wr_pos;
+			priv->rx_bad_cnt = 0;
+			return -ENODATA;
+		}
+
+		if (loongson_canfd_hdr_plausible(h0, h1)) {
+			priv->rx_rd_pos = pos;
+			priv->rx_bad_cnt = 0;
+			return 1;
+		}
+	}
+
+	/* No plausible header nearby: resync on the write pointer */
+	loongson_canfd_dma_sync_wr(priv);
+	priv->rx_rd_pos = priv->rx_wr_pos;
+	priv->rx_bad_cnt = 0;
+	return -ENODATA;
+}
+
+/**
+ * loongson_canfd_rx_one_frame_dma() - Process one RX step in DMA mode
+ * @ndev: Network device
+ *
+ * Return:
+ *   1        - one frame consumed (delivered or dropped on purpose)
+ *   0        - ring pointers were resynced, nothing consumed
+ *   -EAGAIN  - header read but payload not yet resident in the ring
+ *   -ENODATA - a run of malformed frames was detected and the read
+ *              position was moved to the DMA write position
+ */
+static int loongson_canfd_rx_one_frame_dma(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct net_device_stats *stats = &ndev->stats;
+	u64 avail, saved_rd_pos = priv->rx_rd_pos;
+	struct loongson_canfd_rx_desc d;
+	int ret;
+
+	/* Guard against unsigned underflow: rd past wr means desync */
+	if (priv->rx_wr_pos < priv->rx_rd_pos) {
+		netdev_warn(ndev,
+			    "RX pointer inversion: rd=%llu wr=%llu, resync\n",
+			    priv->rx_rd_pos, priv->rx_wr_pos);
+
+		scoped_guard(spinlock_irqsave, &priv->stats_lock) {
+			stats->rx_dropped++;
+		}
+
+		priv->rx_rd_pos = priv->rx_wr_pos;
+		priv->rx_bad_cnt = 0;
+		return 0;
+	}
+
+	avail = priv->rx_wr_pos - priv->rx_rd_pos;
+
+	if (avail > LOONGSON_CANFD_RXDMA_NUM)
+		return loongson_canfd_rx_one_frame_dma_overrun(ndev, avail);
+
+	if (avail < 2)
+		return -EAGAIN;
+
+	if (!loongson_canfd_rx_hdr_valid(ndev, &d))
+		return loongson_canfd_rx_one_frame_dma_bad_hdr(ndev, &d,
+							     saved_rd_pos);
+
+	priv->rx_bad_cnt = 0;
+
+	/*
+	 * Re-sample the DMA position: it may have advanced while the
+	 * header words were being read. If the payload is not fully
+	 * resident yet, roll the read position back and let NAPI retry.
+	 */
+	loongson_canfd_dma_sync_wr(priv);
+
+	if (priv->rx_wr_pos < priv->rx_rd_pos) {
+		priv->rx_rd_pos = priv->rx_wr_pos;
+		return 0;
+	}
+
+	if (priv->rx_wr_pos - priv->rx_rd_pos < d.rwcnt - 2) {
+		priv->rx_rd_pos = saved_rd_pos;
+		return -EAGAIN;
+	}
+
+	ret = loongson_canfd_rx_deliver(ndev, &d);
+	if (ret != -ENOMEM)
+		return 1;
+
+	stats->rx_dropped++;
+	priv->rx_rd_pos = saved_rd_pos + 1;
+	if (++priv->rx_bad_cnt >= 64) {
+		priv->rx_rd_pos = priv->rx_wr_pos;
+		priv->rx_bad_cnt = 0;
+		return -ENODATA;
+	}
 	return 1;
 }
 
@@ -1532,20 +2036,52 @@ static bool loongson_canfd_rx_overrun_event(struct net_device *ndev)
  * @can_complete: True if the poll loop saw an empty hardware FIFO
  * @work_done: Frames processed during this poll
  *
- * The RX interrupt is re-armed only after the loop actually quiesced;
- * a quota-limited exit keeps NAPI scheduled to poll again.
+ * A DMADI that fires between the last rx_is_empty_dma() call and
+ * napi_complete_done() is W1C'd by the ISR but its napi_schedule() is
+ * swallowed by NAPI_STATE_SCHED. Re-syncing the DMA write pointer and
+ * comparing against rx_rd_pos catches that case.
  */
 static void loongson_canfd_rx_napi_done(struct napi_struct *napi,
 					bool can_complete, int work_done)
 {
 	struct net_device *ndev = napi->dev;
 	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	u32 rx_stat;
 
 	if (!can_complete || !napi_complete_done(napi, work_done))
 		return;
 
-	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
-		     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_RBNEI));
+	/*
+	 * Catch a DMADI that was swallowed while SCHED was still set,
+	 * and a stale partial header that rx_is_empty_dma() skipped.
+	 */
+	if (priv->rx_ch) {
+		loongson_canfd_dma_sync_wr(priv);
+		if (priv->rx_wr_pos > priv->rx_rd_pos) {
+			napi_schedule(napi);
+			return;
+		}
+	}
+
+	/*
+	 * Flush an overrun latched while DOI was masked (e.g. during
+	 * NAPI). DOR is sticky and the handler is idempotent, so this
+	 * costs one regmap_read in the common case.
+	 */
+	loongson_canfd_rx_overrun_event(ndev);
+
+	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
+
+	if (FIELD_GET(REG_RX_STAT_RXFRC, rx_stat) == 0 &&
+	    (rx_stat & REG_RX_STAT_RXE)) {
+		/* Quiescent: unmask RBNEI for the next empty -> non-empty edge */
+		regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
+			     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_RBNEI));
+		return;
+	}
+
+	/* FIFO still holds data: keep NAPI scheduled, RBNEI stays masked */
+	napi_schedule(napi);
 }
 
 /**
@@ -1559,18 +2095,45 @@ static int loongson_canfd_rx_napi(struct napi_struct *napi, int quota)
 {
 	struct net_device *ndev = napi->dev;
 	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	int dma_wait_retry = 0, work_done = 0;
 	bool can_complete = false;
-	int work_done = 0;
-	int ret;
 
 	while (work_done < quota) {
+		int ret;
+
 		/* FIFO empty: nothing left to consume */
-		if (loongson_canfd_rx_is_empty_fifo(priv)) {
+		if (priv->rx_is_empty(priv)) {
 			can_complete = true;
 			break;
 		}
 
-		ret = loongson_canfd_rx_one_frame_fifo(ndev);
+		ret = priv->process_rx(ndev);
+
+		if (ret == -EAGAIN) {
+			/*
+			 * FIFO holds a frame but the DMA engine has not
+			 * moved its payload yet. Spin briefly; the RX FIFO
+			 * is 80 bytes deep, so a long spin only delays the
+			 * next cycle. Past the budget quiesce and let
+			 * rx_napi_done() reschedule.
+			 */
+			if (dma_wait_retry++ < LOONGSON_CANFD_DMA_WAIT_RETRY_MAX) {
+				cpu_relax();
+				continue;
+			}
+
+			can_complete = true;
+			break;
+		}
+
+		if (ret == -ENODATA) {
+			/* Pointer realigned by process_rx(), quiesce */
+			can_complete = true;
+			break;
+		}
+
+		dma_wait_retry = 0;
+
 		if (ret == 1)
 			work_done++;
 	}
@@ -1847,6 +2410,10 @@ static void loongson_canfd_err_interrupt(struct net_device *ndev, u32 isr)
  * optional HW TX timestamp) and counts it sent; TX_BS_FAIL and
  * TX_BS_CANCEL free the skb as dropped. Clears the completion record
  * per slot and wakes the queue if a buffer became free.
+ *
+ * RBNEI is serviced by masking it, resuming the paused RX DMA channel
+ * (no-op in poll mode) and scheduling NAPI. DMADI is never enabled, so
+ * no DMA completion IRQ is expected in the RX path.
  */
 static void loongson_canfd_tx_interrupt(struct net_device *ndev)
 {
@@ -1951,6 +2518,11 @@ static void loongson_canfd_tx_interrupt(struct net_device *ndev)
  * @irq: IRQ number
  * @dev_id: Device identifier (net_device)
  *
+ * RBNEI is mask-clear-schedule; it is unmasked later by
+ * rx_napi_done() once the FIFO is empty. DMADI is edge-triggered
+ * (W1C) and simply schedules NAPI; the channel is never paused, so
+ * nothing to resume.
+ *
  * Return: IRQ_HANDLED if interrupt was handled, IRQ_NONE otherwise.
  */
 static irqreturn_t loongson_canfd_interrupt(int irq, void *dev_id)
@@ -1977,6 +2549,13 @@ static irqreturn_t loongson_canfd_interrupt(int irq, void *dev_id)
 			napi_schedule(&priv->napi);
 		}
 
+		if (isr & REG_INT_STAT_DMADI) {
+			/* W1C: fresh data in the ring, schedule NAPI */
+			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
+				     REG_INT_STAT_DMADI);
+			napi_schedule(&priv->napi);
+		}
+
 		if (isr & REG_INT_STAT_TXBHCI) {
 			/*
 			 * Mask-clear-handle-unmask: a TX completing during
@@ -2098,6 +2677,9 @@ static netdev_tx_t loongson_canfd_start_xmit(struct sk_buff *skb, struct net_dev
  * loongson_canfd_open() - Open the network device
  * @ndev: Network device
  *
+ * Start DMA first, then enable the controller: as soon as chip_start()
+ * pushes data into RX_DATA, the DMA must already be ready.
+ *
  * Return: 0 on success, negative error on failure.
  */
 static int loongson_canfd_open(struct net_device *ndev)
@@ -2121,10 +2703,14 @@ static int loongson_canfd_open(struct net_device *ndev)
 		goto exit_napi_disable;
 	}
 
+	ret = loongson_canfd_rxdma_start(ndev);
+	if (ret)
+		goto exit_free_irq;
+
 	ret = loongson_canfd_chip_start(ndev);
 	if (ret < 0) {
 		netdev_err(ndev, "Chip start failed\n");
-		goto exit_free_irq;
+		goto exit_stop_dma;
 	}
 
 	netdev_info(ndev, "Loongson CANFD device registered\n");
@@ -2132,6 +2718,8 @@ static int loongson_canfd_open(struct net_device *ndev)
 
 	return 0;
 
+exit_stop_dma:
+	loongson_canfd_rxdma_stop(priv);
 exit_free_irq:
 	free_irq(ndev->irq, ndev);
 exit_napi_disable:
@@ -2165,6 +2753,7 @@ static int loongson_canfd_close(struct net_device *ndev)
 	cancel_delayed_work_sync(&priv->can.restart_work);
 
 	loongson_canfd_chip_stop(ndev);
+	loongson_canfd_rxdma_stop(priv);
 	free_irq(ndev->irq, ndev);
 	close_candev(ndev);
 
@@ -2316,6 +2905,66 @@ static void loongson_canfd_setup_can_priv(struct net_device *ndev, u32 clk_rate)
 				       CAN_CTRLMODE_CC_LEN8_DLC;
 }
 
+/**
+ * loongson_canfd_rxdma_setup() - Decide RX mode and arm DMA if available
+ * @ndev: Network device
+ *
+ * On return @priv->rx_ch is either a valid channel (DMA mode) or
+ * NULL (poll mode or failure); it is never an ERR_PTR.
+ *
+ * Return: 0 on success (DMA or poll mode), -EPROBE_DEFER if the DMA
+ *         controller is not ready yet, negative error code otherwise.
+ */
+static int loongson_canfd_rxdma_setup(struct net_device *ndev)
+{
+	struct loongson_canfd_priv *priv = netdev_priv(ndev);
+	struct device *dev = ndev->dev.parent;
+	int ret;
+
+	priv->rx_ch = dma_request_chan(dev, "rx");
+	if (IS_ERR(priv->rx_ch)) {
+		ret = PTR_ERR(priv->rx_ch);
+		priv->rx_ch = NULL;
+
+		if (ret == -EPROBE_DEFER)
+			return -EPROBE_DEFER;
+
+		/*
+		 * -ENODEV: firmware describes no "rx" channel. Normal on
+		 * poll-mode platforms, so log at info level, not warn.
+		 */
+		if (ret == -ENODEV) {
+			dev_info(dev,
+				 "No RX DMA channel, using FIFO poll mode");
+			return 0;
+		}
+
+		/* Anything else is a real DMA setup problem */
+		return dev_err_probe(dev, ret,
+				     "Failed to request RX DMA channel");
+	}
+
+	ret = loongson_canfd_rxdma_init(ndev);
+	if (ret) {
+		/*
+		 * rxdma_init() has already released the channel and set
+		 * rx_ch to NULL on both paths.
+		 */
+		if (ret != -ENOMEM)
+			return ret;
+
+		dev_warn(dev,
+			 "RX DMA buffer alloc failed, using FIFO poll mode");
+		return 0;
+	}
+
+	priv->read_rx_word = loongson_canfd_read_rx_word_dma;
+	priv->rx_is_empty = loongson_canfd_rx_is_empty_dma;
+	priv->process_rx = loongson_canfd_rx_one_frame_dma;
+
+	return 0;
+}
+
 static int loongson_canfd_probe(struct platform_device *pdev)
 {
 	struct loongson_canfd_priv *priv;
@@ -2363,18 +3012,28 @@ static int loongson_canfd_probe(struct platform_device *pdev)
 	ndev->ethtool_ops = &loongson_canfd_ethtool_ops;
 	SET_NETDEV_DEV(ndev, dev);
 
+	priv->read_rx_word = loongson_canfd_read_rx_word_fifo;
+	priv->rx_is_empty = loongson_canfd_rx_is_empty_fifo;
+	priv->process_rx = loongson_canfd_rx_one_frame_fifo;
+
+	ret = loongson_canfd_rxdma_setup(ndev);
+	if (ret)
+		goto err_candev_free;
+
 	netif_napi_add(ndev, &priv->napi, loongson_canfd_rx_napi);
 
 	ret = register_candev(ndev);
 	if (ret) {
 		dev_err(dev, "register_candev failed with %d", ret);
-		goto err_candev_free;
+		goto err_napi_del;
 	}
 
 	return 0;
 
-err_candev_free:
+err_napi_del:
 	netif_napi_del(&priv->napi);
+	loongson_canfd_rxdma_remove(priv, &pdev->dev);
+err_candev_free:
 	free_candev(ndev);
 	return ret;
 }
@@ -2390,6 +3049,7 @@ static void loongson_canfd_remove(struct platform_device *pdev)
 
 	unregister_candev(ndev);
 	netif_napi_del(&priv->napi);
+	loongson_canfd_rxdma_remove(priv, &pdev->dev);
 	free_candev(ndev);
 }
 
@@ -2409,6 +3069,7 @@ static struct platform_driver loongson_canfd_driver = {
 };
 module_platform_driver(loongson_canfd_driver);
 
+MODULE_SOFTDEP("pre: loongson2-apb-cmc-dma");
 MODULE_AUTHOR("Loongson Technology Corporation Limited");
 MODULE_DESCRIPTION("Loongson CAN-FD Controller driver");
 MODULE_LICENSE("GPL");
-- 
2.52.0


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 1/2] can: Add Loongson CAN-FD controller driver
  2026-11-09  8:42 ` [PATCH v9 1/2] can: " Binbin Zhou
@ 2026-11-09  9:31   ` sashiko-bot
  2026-11-10 22:20   ` netdev-bot+sashiko
  1 sibling, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09  9:31 UTC (permalink / raw)
  To: Binbin Zhou
  Cc: linux-can, Marc Kleine-Budde, Oliver Hartkopp, Vincent Mailhol,
	Oleksij Rempel

Thank you for your contribution! Sashiko AI review found 16 potential
issues to consider:
- [High] can: loongson_canfd: use-after-free in
  loongson_canfd_start_xmit() after echo
- [Medium] can: loongson_canfd: use of can_dropped_invalid_skb() in
  start_xmit()
- [Medium] can: loongson_canfd: CANFD_FDF cleared in
  loongson_canfd_rx_deliver()
- [Medium] can: loongson_canfd: incomplete free check in
  loongson_canfd_txbuf_freed()
- [Medium] can: loongson_canfd: TOCTOU race waking TX queue outside
  priv->tx_lock
- [Medium] can: loongson_canfd: broken error frame flags and stats in
  state/overrun paths
- [Medium] can: loongson_canfd: missing TX queue and chip stop on
  CAN_STATE_BUS_OFF
- [Medium] can: loongson_canfd: bogus error skb stats and race on
  ndev->stats
- [Medium] can: loongson_canfd: race enabling IRQs before state init in
  loongson_canfd_chip_start()
- [Medium] can: loongson_canfd: teardown race with IRQ and restart_work
  in loongson_canfd_close()
- [Medium] can: loongson_canfd: hwts capabilities hidden when down in
  loongson_canfd_get_ts_info()
- [Medium] can: loongson_canfd: broken REGCACHE_MAPLE config on banked
  TX and FIFO registers
- [Medium] can: loongson_canfd: prop_seg > 127 truncated in
  loongson_canfd_set_btr()
- [Medium] can: loongson_canfd: RX FIFO desync and tautological check in
  rx_hdr_valid()
- [Medium] can: loongson_canfd: missing mode checks in
  loongson_canfd_start_xmit()
- [Low] can: loongson_canfd: tautological SFF ID check in
  loongson_canfd_hdr_plausible()
--

commit 081a5a8aea730be1bb8b197fafbcd027960619e3
Author: Binbin Zhou <zhoubinbin@loongson.cn>

can: Add Loongson CAN-FD controller driver

This commit adds a driver for the CAN-FD controller integrated in the
Loongson-2K series SoCs, supporting CAN 2.0B, CAN FD, NAPI RX, TX echo,
error reporting, and hardware timestamps.

> diff --git a/drivers/net/can/loongson_canfd.c b/drivers/net/can/loongson_canfd.c
> new file mode 100644
> index 0000000000000..7a4d8df7815ac
> --- /dev/null
> +++ b/drivers/net/can/loongson_canfd.c

[ ... ]

> +static void loongson_canfd_set_btr(struct net_device *ndev,
> +				   struct can_bittiming *bt, bool nominal)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 prop = bt->prop_seg, phase1 = bt->phase_seg1;
> +	u32 ph1_max = FIELD_MAX(REG_BTR_PH1);
> +	u32 btr = 0;
> +
> +	/* PROP is 7 bits but PH1 only 6; move the excess into PROP */
> +	if (phase1 > ph1_max) {
> +		prop += phase1 - ph1_max;
> +		phase1 = ph1_max;
> +	}
> +
> +	if (nominal) {
> +		btr = FIELD_PREP(REG_BTR_PROP, prop) |

[Severity: Medium]
Can FIELD_PREP(REG_BTR_PROP, prop) and FIELD_PREP(REG_BTR_FD_PROP, prop)
truncate prop when manual bit-timing parameters specify prop_seg > 127?

loongson_canfd_bit_timing and loongson_canfd_bit_timing_data advertise
tseg1_max = 190, and can_fixup_bittiming() only enforces
prop_seg + phase_seg1 <= tseg1_max.  If userspace sets prop_seg in
128..189, the excess above FIELD_MAX(REG_BTR_PROP) (127) is not moved
into phase1 and is masked off by FIELD_PREP(), programming the wrong bit
rate and sample point.

[ ... ]

> +static int loongson_canfd_chip_start(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 int_ena, int_msk;
> +	int ret;
> +
> +	ret = loongson_canfd_set_bittiming(ndev);
> +	if (ret < 0)
> +		return ret;
> +
> +	/* Programs BTR_FD and SSP_CFG together */
> +	ret = loongson_canfd_set_data_bittiming(ndev);
> +	if (ret < 0)
> +		return ret;
> +
> +	loongson_canfd_apply_ctrlmode(priv);
> +
> +	int_ena = REG_INT_STAT_RBNEI | REG_INT_STAT_TXBHCI |
> +		  REG_INT_STAT_EWLI  | REG_INT_STAT_FCSI   |
> +		  REG_INT_STAT_DOI;
> +
> +	if (priv->can.ctrlmode & CAN_CTRLMODE_BERR_REPORTING)
> +		int_ena |= REG_INT_STAT_ALI | REG_INT_STAT_BEI;
> +
> +	int_msk = REG_INT_MASK_SET & ~int_ena;
> +
> +	/* Clear stale mask/ena bits left by disable_interrupts() */
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, REG_INT_MASK_CLR);
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, REG_INT_ENA_CLR);
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, int_msk);
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, int_ena);
> +
> +	loongson_canfd_hwts_setup(ndev);
> +
> +	priv->can.state = CAN_STATE_ERROR_ACTIVE;
> +
> +	regmap_update_bits(priv->regmap, LOONGSON_CANFD_CONF, REG_CONF_ENA,
> +			   REG_CONF_ENA);
> +
> +	return 0;
> +}

[Severity: Medium]
Can enabling LOONGSON_CANFD_INT_ENA before setting priv->can.state =
CAN_STATE_ERROR_ACTIVE trigger a spurious state change report?

CPU0                              <hardirq>
loongson_canfd_chip_start()
  regmap_write(..., INT_MASK, int_msk)
  regmap_write(..., INT_ENA, int_ena)
                                  loongson_canfd_interrupt()
                                    loongson_canfd_err_interrupt()
                                      priv->can.state != state
                                      loongson_canfd_report_state_change()
  loongson_canfd_hwts_setup()
  priv->can.state = CAN_STATE_ERROR_ACTIVE;

loongson_canfd_reset() latches REG_INT_STAT_FCSI on reset to Error Active,
and loongson_canfd_err_interrupt() relies on priv->can.state ==
CAN_STATE_ERROR_ACTIVE to ignore that initial interrupt.  Because the IRQ
is already registered in loongson_canfd_open() and
loongson_canfd_do_set_mode(), FCSI can fire while priv->can.state is still
CAN_STATE_STOPPED or CAN_STATE_BUS_OFF and before hwts is initialized.

[ ... ]

> +static bool loongson_canfd_txbuf_freed(struct loongson_canfd_priv *priv)
> +{
> +	return !regmap_test_bits(priv->regmap, LOONGSON_CANFD_TX_STAT, REG_TX_STAT_BRP);
> +}

[Severity: Medium]
Does loongson_canfd_txbuf_freed() return true even when no TX buffer is
writable?

loongson_canfd_find_free_txbuf() requires a slot's BRP bit to be clear,
its 2-bit BS field to be TX_BS_IDLE, and priv->can.echo_skb[i] to be NULL.
When a frame finishes on the wire, hardware clears its BRP bit and sets BS
to TX_BS_VALID or TX_BS_FAIL until loongson_canfd_tx_interrupt() runs
TXT_CMD_SR_CLEAN.  Checking only REG_TX_STAT_BRP leaves the queue awake in
loongson_canfd_start_xmit() when the last writable slot is used while
another slot awaits ISR cleanup, causing the next start_xmit() call to
return NETDEV_TX_BUSY.

[ ... ]

> +	for (unsigned int i = 0; i < cf->len; i += LOONGSON_CANFD_DW_BYTE) {
> +		unsigned int remain = cf->len - i;
> +		u32 word;
> +
> +		if (remain >= LOONGSON_CANFD_DW_BYTE) {
> +			word = get_unaligned_le32(cf->data + i);

[Severity: Medium]
Can an ETH_P_CANXL skb cause loongson_canfd_insert_frame() to read past
skb->data and write past LOONGSON_CANFD_TX_DATA_18?

loongson_canfd_start_xmit() validates frames with
can_dropped_invalid_skb() instead of can_dev_dropped_skb().  That skips
the CAN_CTRLMODE_LISTENONLY and CAN_CTRLMODE_FD checks, allowing frames
from non-RAW sockets (such as can-bcm or can-isotp) to fill the hardware
TX buffers while in listen-only mode.  In addition, if an ETH_P_CANXL skb
is sent via AF_PACKET, cf->len aliases canxl_frame::flags (which has
CANXL_XLF = 0x80 set), so this loop reads out-of-bounds past small CAN XL
skbs and writes registers past LOONGSON_CANFD_TX_DATA_18.

[ ... ]

> +static bool loongson_canfd_hdr_plausible(u32 hdr0, u32 hdr1)
> +{
> +	u32 rwcnt  = FIELD_GET(REG_FRAME_META1_RWCNT, hdr1);
> +	u32 dlc    = FIELD_GET(REG_FRAME_META1_DLC,   hdr1);
> +	bool isfd  = !!(hdr1 & REG_FRAME_META1_FDF);
> +	bool isrtr = !!(hdr0 & REG_FRAME_META0_RTR);
> +	u32 payload_len, expected_rwcnt;
> +
> +	/* 2 header words + payload, within CAN FD limits */
> +	if (rwcnt < 2 || rwcnt > 2 + CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE)
> +		return false;
> +
> +	/* RTR is a Classic CAN concept */
> +	if (isfd && isrtr)
> +		return false;
> +
> +	/* Standard ID fits in 11 bits */
> +	if (!(hdr0 & REG_FRAME_META0_XDT) &&
> +	    FIELD_GET(REG_FRAME_META0_ID_BASE, hdr0) > CAN_SFF_MASK)
> +		return false;

[Severity: Low]
Can FIELD_GET(REG_FRAME_META0_ID_BASE, hdr0) ever exceed CAN_SFF_MASK?
REG_FRAME_META0_ID_BASE is GENMASK(28, 18), an 11-bit field whose maximum
value is already CAN_SFF_MASK (0x7ff), whereas REG_FRAME_META0_ID_EXT
(GENMASK(17, 0)) is not checked when REG_FRAME_META0_XDT is clear.

[ ... ]

> +static void loongson_canfd_rx_note_bad(struct net_device *ndev, u32 rwcnt)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 words, data;
> +
> +	ndev->stats.rx_dropped++;
> +
> +	words = rwcnt > 2 ?
> +		min_t(u32, rwcnt - 2,
> +		      CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE) : 0;
> +	while (words--)
> +		regmap_read(priv->regmap, LOONGSON_CANFD_RX_DATA, &data);
> +}
> +
> +/**
> + * loongson_canfd_rx_hdr_valid() - Validate a frame header pair
> + * @ndev: Network device
> + * @d: Output descriptor, filled with all parsed fields
> + *
> + * Reads and sanity-checks the two header words. The caller must drain
> + * the remaining payload words when false is returned.
> + *
> + * Return: true if the header is valid, false otherwise.
> + */
> +static bool loongson_canfd_rx_hdr_valid(struct net_device *ndev,
> +					struct loongson_canfd_rx_desc *d)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	d->hdr0 = loongson_canfd_read_rx_word_fifo(priv);
> +	d->hdr1 = loongson_canfd_read_rx_word_fifo(priv);
> +
> +	d->rwcnt = FIELD_GET(REG_FRAME_META1_RWCNT, d->hdr1);
> +	if (d->rwcnt < 2) {
> +		/* Hand the caller a worst-case drain length */
> +		d->rwcnt = 2 + CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE;
> +		return false;
> +	}

[Severity: Medium]
Does setting d->rwcnt to 18 when d->rwcnt < 2 desynchronize the 80-byte
hardware RX FIFO?

When d->rwcnt < 2, loongson_canfd_rx_note_bad() unconditionally reads 16
words from LOONGSON_CANFD_RX_DATA without checking
loongson_canfd_rx_is_empty_fifo(), which consumes following valid frames in
the FIFO and leaves the next read mid-frame.  Also, the standard-ID check
in loongson_canfd_hdr_plausible() compares the 11-bit
REG_FRAME_META0_ID_BASE field against 0x7ff instead of checking whether
REG_FRAME_META0_ID_EXT is non-zero.

[ ... ]

> +	cfd->flags = 0;
> +
> +	if (d->is_can_fd) {
> +		if (d->hdr1 & REG_FRAME_META1_BRS)
> +			cfd->flags |= CANFD_BRS;
> +		if (d->hdr0 & REG_FRAME_META0_ESI)
> +			cfd->flags |= CANFD_ESI;
> +	} else if (d->is_rtr) {
> +		cfd->can_id |= CAN_RTR_FLAG;
> +	}

[Severity: Medium]
Does setting cfd->flags = 0 clear the mandatory CANFD_FDF flag that
alloc_canfd_skb() initialized on received CAN FD frames?

[ ... ]

> +	skb = alloc_can_err_skb(ndev, &cf);
> +	if (skb) {
> +		cf->can_id |= CAN_ERR_CRTL;
> +		cf->data[1] = CAN_ERR_CRTL_RX_OVERFLOW;
> +
> +		scoped_guard(spinlock_irqsave, &priv->stats_lock) {
> +			stats->rx_packets++;
> +			stats->rx_bytes += cf->len;
> +		}
> +
> +		/* Hard IRQ: use netif_rx(), not netif_receive_skb() */
> +		netif_rx(skb);
> +	}

[Severity: Medium]
As reported by Vincent Mailhol on v2 [1], should synthetic SocketCAN error
frames be excluded from stats->rx_packets and stats->rx_bytes here?  Also,
updating stats->rx_packets and stats->rx_bytes under priv->stats_lock in
hardirq context races with the unlocked updates to the same counters in
loongson_canfd_rx_deliver() during NAPI polling.

[ ... ]

> +	/* Stats and BUS_OFF handling must not depend on skb allocation */
> +	switch (new_state) {
> +	case CAN_STATE_BUS_OFF:
> +		priv->can.can_stats.bus_off++;
> +		can_bus_off(ndev);
> +		break;

[Severity: Medium]
Does entering CAN_STATE_BUS_OFF without stopping the TX queue and
controller allow loongson_canfd_start_xmit() and
loongson_canfd_tx_interrupt() to race with bus-off recovery?

CPU0                              CPU1
can_restart_work()
  can_restart()
    can_flush_echo_skb()
      kfree_skb(priv->echo_skb[i])
                                  loongson_canfd_tx_interrupt()
                                    can_free_echo_skb()  <-- double free
    netif_carrier_on()
    loongson_canfd_do_set_mode()
      loongson_canfd_reset()
                                  loongson_canfd_start_xmit()

can_bus_off() only calls netif_carrier_off(), which defers qdisc
deactivation to linkwatch_work, and can_restart() flushes echo_skb[]
without holding priv->tx_lock before calling netif_carrier_on() and
do_set_mode(CAN_MODE_START).

> +	case CAN_STATE_ERROR_PASSIVE:
> +		priv->can.can_stats.error_passive++;
> +		break;
> +	case CAN_STATE_ERROR_WARNING:
> +		priv->can.can_stats.error_warning++;
> +		break;
> +	default:
> +		break;
> +	}
> +
> +	skb = alloc_can_err_skb(ndev, &cf);
> +	if (!skb)
> +		return;
> +
> +	switch (new_state) {
> +	case CAN_STATE_BUS_OFF:
> +		cf->can_id |= CAN_ERR_BUSOFF;
> +		goto send;
> +	case CAN_STATE_ERROR_ACTIVE:
> +		cf->can_id |= CAN_ERR_CNT;
> +		cf->data[1] = CAN_ERR_CRTL_ACTIVE;
> +		break;

[Severity: Medium]
Should CAN_STATE_ERROR_ACTIVE also set CAN_ERR_CRTL in cf->can_id when
reporting CAN_ERR_CRTL_ACTIVE in cf->data[1], and should error_passive and
error_warning only be incremented when transitioning to a worse state?

Overwriting priv->can.state before checking the transition direction
counts recoveries (such as CAN_STATE_ERROR_PASSIVE down to
CAN_STATE_ERROR_WARNING) as new error events.  In addition,
loongson_canfd_rx_overrun_event() adds cf->len of an error skb to
stats->rx_bytes under priv->stats_lock while loongson_canfd_rx_deliver()
updates rx_packets and rx_bytes without priv->stats_lock.

[ ... ]

> +			case TX_BS_VALID:
> +				u32 dummy_len;
> +
> +				/*
> +				 * The controller does not latch a per-frame
> +				 * TX stamp, so the instant the handler reads
> +				 * the counter is the best estimate of the
> +				 * end of the frame.
> +				 */
> +				skb = priv->can.echo_skb[buf_id];
> +				if (skb &&
> +				    (skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS) &&
> +				    READ_ONCE(priv->hwts.ready))
> +					skb_hwtstamps(skb)->hwtstamp =
> +							loongson_canfd_hwts_now(priv);
> +
> +				dummy_len = can_get_echo_skb(ndev, buf_id, NULL);
> +				(void)dummy_len;
> +				stats->tx_packets++;
> +				break;

[ ... ]

> +		if (!processed)
> +			netdev_warn_once(ndev, "Spurious TX interrupt\n");
> +
> +		wake = loongson_canfd_txbuf_freed(priv);
> +	}
> +
> +	if (wake)
> +		netif_wake_queue(ndev);
> +}

[Severity: Medium]
Can calling netif_wake_queue() after dropping priv->tx_lock re-wake the TX
queue after loongson_canfd_start_xmit() has just filled the last buffer
and stopped it?

CPU0                              CPU1
loongson_canfd_tx_interrupt()
  lock(&priv->tx_lock)
  wake = loongson_canfd_txbuf_freed()
  unlock(&priv->tx_lock)
                                  loongson_canfd_start_xmit()
                                    lock(&priv->tx_lock)
                                    /* consumes last free TX buffer */
                                    netif_stop_queue(ndev)
                                    unlock(&priv->tx_lock)
  netif_wake_queue(ndev)  <-- wakes full queue

[ ... ]

> +static netdev_tx_t loongson_canfd_start_xmit(struct sk_buff *skb, struct net_device *ndev)
> +{
> +	struct canfd_frame *cf = (struct canfd_frame *)skb->data;
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	int buf_id, ret;
> +
> +	if (can_dropped_invalid_skb(ndev, skb))
> +		return NETDEV_TX_OK;

[Severity: Medium]
Should loongson_canfd_start_xmit() call can_dev_dropped_skb() instead of
can_dropped_invalid_skb()?

can_dropped_invalid_skb() is for virtual CAN drivers and does not check
CAN_CTRLMODE_LISTENONLY or CAN_CTRLMODE_FD, allowing frames sent via
non-RAW sockets in listen-only mode or CAN FD/XL frames sent via AF_PACKET
to reach loongson_canfd_insert_frame().

[ ... ]

> +	can_put_echo_skb(skb, ndev, buf_id, 0);
> +
> +	if (!(cf->can_id & CAN_RTR_FLAG))
> +		stats->tx_bytes += cf->len;

[Severity: High]
Does reading cf->can_id and cf->len after can_put_echo_skb() cause a
use-after-free of skb?

can_put_echo_skb() consumes or frees skb (for example via kfree_skb() when
IFF_ECHO is cleared or when skb_clone() fails in can_create_echo_skb()),
and cf points into skb->data.  Also, incrementing stats->tx_bytes here
instead of adding the length returned by can_get_echo_skb() in
loongson_canfd_tx_interrupt() counts bytes for frames that later fail, are
cancelled, or are flushed.

[ ... ]

> +static int loongson_canfd_close(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	netif_stop_queue(ndev);
> +	napi_disable(&priv->napi);
> +
> +	/*
> +	 * Cancel a pending bus-off restart before tearing down IRQ/DMA:
> +	 * do_set_mode() re-arms both, so a restart firing after
> +	 * free_irq() would touch released resources. close_candev()\
> +	 * does the same but only after free_irq().
> +	 */
> +	cancel_delayed_work_sync(&priv->can.restart_work);
> +
> +	loongson_canfd_chip_stop(ndev);
> +	free_irq(ndev->irq, ndev);
> +	close_candev(ndev);
> +
> +	return 0;
> +}

[Severity: Medium]
Can an interrupt firing before loongson_canfd_chip_stop() and free_irq()
re-schedule restart_work or schedule NAPI after napi_disable()?

CPU0                              <hardirq>
loongson_canfd_close()
  napi_disable(&priv->napi)
  cancel_delayed_work_sync(&restart_work)
                                  loongson_canfd_interrupt()
                                    loongson_canfd_err_interrupt()
                                      can_bus_off()
                                        schedule_delayed_work(&restart_work)
  loongson_canfd_chip_stop()
                                  can_restart_work()
                                    loongson_canfd_do_set_mode(CAN_MODE_START)
                                      loongson_canfd_chip_start()  <-- restarts stopped chip
  free_irq()

Stopping the chip and freeing the IRQ before cancelling restart_work and
disabling NAPI avoids the ISR re-arming either during teardown.

[ ... ]

> +static int loongson_canfd_get_ts_info(struct net_device *ndev,
> +				      struct kernel_ethtool_ts_info *info)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	info->so_timestamping = SOF_TIMESTAMPING_SOFTWARE |
> +				SOF_TIMESTAMPING_TX_SOFTWARE |
> +				SOF_TIMESTAMPING_RX_SOFTWARE;
> +
> +	info->phc_index = -1;
> +
> +	if (READ_ONCE(priv->hwts.ready)) {
> +		info->so_timestamping |= SOF_TIMESTAMPING_TX_HARDWARE |
> +					 SOF_TIMESTAMPING_RX_HARDWARE |
> +					 SOF_TIMESTAMPING_RAW_HARDWARE;
> +		info->tx_types = BIT(HWTSTAMP_TX_ON);
> +		info->rx_filters = BIT(HWTSTAMP_FILTER_ALL);
> +	}
> +
> +	return 0;
> +}

[Severity: Medium]
Does checking priv->hwts.ready in loongson_canfd_get_ts_info() hide the
device's hardware timestamping capabilities whenever the interface is down?

priv->hwts.ready is only true between loongson_canfd_chip_start() and
loongson_canfd_chip_stop(), so querying ethtool timestamping info while the
interface is down omits the hardware timestamping flags and filters even
though can_hwtstamp_get() and can_hwtstamp_set() are unconditionally wired
up (see can_ethtool_op_get_ts_info_hwts()).

[ ... ]

> +static bool loongson_canfd_volatile_reg(struct device *dev, unsigned int reg)
> +{
> +	switch (reg) {
> +	case LOONGSON_CANFD_MODE:
> +	case LOONGSON_CANFD_CONF:
> +	case LOONGSON_CANFD_STAT:
> +	case LOONGSON_CANFD_CMD:
> +	case LOONGSON_CANFD_INT_STAT:
> +	case LOONGSON_CANFD_INT_ENA:
> +	case LOONGSON_CANFD_INT_MASK:
> +	case LOONGSON_CANFD_ERL:
> +	case LOONGSON_CANFD_FSTAT:
> +	case LOONGSON_CANFD_ERC:
> +	case LOONGSON_CANFD_ERR_CAPT:
> +	case LOONGSON_CANFD_ALC:
> +	case LOONGSON_CANFD_TRV_DLY:
> +	case LOONGSON_CANFD_TX_FR_CNT:
> +	case LOONGSON_CANFD_TS:
> +	case LOONGSON_CANFD_RX_STAT:
> +	case LOONGSON_CANFD_RX_DATA:
> +	case LOONGSON_CANFD_TX_STAT:
> +	case LOONGSON_CANFD_TX_CMD:
> +	case LOONGSON_CANFD_TX_SEL:
> +		return true;
> +	default:
> +		return false;
> +	};
> +}
> +
> +static const struct regmap_config loongson_canfd_regmap = {
> +	.reg_bits	= 32,
> +	.reg_stride	= 4,
> +	.val_bits	= 32,
> +	.wr_table	= &loongson_canfd_reg_table_wr,
> +	.rd_table	= &loongson_canfd_reg_table_rd,
> +	.volatile_reg	= loongson_canfd_volatile_reg,
> +	.max_register	= LOONGSON_CANFD_TX_DATA_18,
> +	.cache_type	= REGCACHE_MAPLE,
> +};

[Severity: Medium]
Why does loongson_canfd_regmap enable REGCACHE_MAPLE when the banked TX
data registers (LOONGSON_CANFD_TX_DATA_1..LOONGSON_CANFD_TX_DATA_18) are
not marked volatile?

Those offsets are a banked window into 8 TX buffers selected by
LOONGSON_CANFD_TX_SEL, and caching them with REGCACHE_MAPLE causes
regmap_write() in loongson_canfd_insert_frame() to perform GFP_ATOMIC
maple-tree allocations under priv->tx_lock (dropping the frame if an
allocation fails).  Also, LOONGSON_CANFD_RX_DATA has no precious_reg
callback, so reading regmap debugfs pops words from the live RX FIFO.

[1] https://lore.kernel.org/r/c9598f85-5b17-4d38-855e-c3840a01452f@kernel.org

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1794213427.git.zhoubinbin@loongson.cn?part=1

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support
  2026-11-09  8:42 ` [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support Binbin Zhou
@ 2026-11-09  9:51   ` sashiko-bot
  2026-11-10 22:20   ` netdev-bot+sashiko
  1 sibling, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-11-09  9:51 UTC (permalink / raw)
  To: Binbin Zhou
  Cc: linux-can, Marc Kleine-Budde, Oliver Hartkopp, Vincent Mailhol,
	Oleksij Rempel

Thank you for your contribution! Sashiko AI review found 11 potential
issues to consider:
- [High] can: loongson_canfd: broken empty check in
  loongson_canfd_rx_is_empty_dma()
- [High] can: loongson_canfd: full-ring discard in
  loongson_canfd_rx_is_empty_dma()
- [High] can: loongson_canfd: -ENOMEM ring desync in
  loongson_canfd_rx_one_frame_dma()
- [High] can: loongson_canfd: unchecked ERR_PTR in
  loongson_canfd_rxdma_start()
- [High] can: loongson_canfd: racy RX DMA restart in
  loongson_canfd_do_set_mode()
- [High] can: loongson_canfd: softirq loop in
  loongson_canfd_rx_one_frame_dma_overrun()
- [Medium] can: loongson_canfd: wrong check order in
  loongson_canfd_rx_napi_done()
- [Medium] can: loongson_canfd: race in
  loongson_canfd_rx_overrun_event() with hard IRQ
- [Low] can: loongson_canfd: inconsistent stats_lock usage on RX
  net_device_stats
- [Low] can: loongson_canfd: dead MMIO and DMA status reads in RX DMA
  recovery helpers
- [Low] can: loongson_canfd: stale RX DMA comment on
  loongson_canfd_tx_interrupt()
--

commit 163f344c5c06e5b35fc3c10b21a31b54fe6b6332
Author: Binbin Zhou <zhoubinbin@loongson.cn>

can: loongson_canfd: Add RXDMA support

This commit adds optional cyclic RX DMA support to the Loongson CAN-FD
driver, falling back to FIFO polling when no RX DMA channel is present.

> diff --git a/drivers/net/can/loongson_canfd.c b/drivers/net/can/loongson_canfd.c
> index 7a4d8df7815ac..b5a5917bc7738 100644
> --- a/drivers/net/can/loongson_canfd.c
> +++ b/drivers/net/can/loongson_canfd.c

[ ... ]

> @@ -504,6 +539,213 @@ static void loongson_canfd_disable_interrupts(struct loongson_canfd_priv *priv)
> +static int loongson_canfd_rxdma_start(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct dma_async_tx_descriptor *desc;
> +
> +	if (!priv->rx_ch)
> +		return 0;
> +
> +	priv->rx_rd_pos = 0;
> +	priv->rx_wr_pos = 0;
> +	priv->rx_wr_off = 0;
> +	priv->rx_bad_cnt = 0;
> +
> +	desc = dmaengine_prep_dma_cyclic(priv->rx_ch, priv->rx_dma_addr,
> +					 LOONGSON_CANFD_RXBUF_SZ,
> +					 LOONGSON_CANFD_RXDMA_PERIOD,
> +					 DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);
> +	if (!desc) {
> +		netdev_err(ndev, "RX DMA cyclic prep failed\n");
> +		return -EBUSY;
> +	}
> +
> +	priv->rx_cookie = dmaengine_submit(desc);
> +	if (dma_submit_error(priv->rx_cookie)) {
> +		netdev_err(ndev, "RX DMA submit failed\n");
> +		return -EIO;
> +	}

[Severity: High]
Can dmaengine_submit(desc) dereference an error pointer here?

The paired loongson2-apb-cmc-dma driver returns an ERR_PTR() from
loongson2_cmc_dma_prep_dma_cyclic() on failure (for example,
ERR_PTR(-ENOMEM) when its GFP_NOWAIT allocation fails) rather than NULL:

drivers/dma/loongson/loongson2-apb-cmc-dma.c:loongson2_cmc_dma_prep_dma_cyclic() {
    ...
	desc = kzalloc_flex(*desc, sg_req, num_periods, GFP_NOWAIT);
	if (!desc)
		return ERR_PTR(-ENOMEM);
    ...
}

Checking only !desc lets an error pointer reach dmaengine_submit(desc) and
crash the kernel.  Also, if dma_submit_error(priv->rx_cookie) is true,
returning -EIO without freeing desc leaks the prepared descriptor.

[ ... ]

> @@ -995,15 +1245,29 @@ static void loongson_canfd_chip_stop(struct net_device *ndev)
>  static int loongson_canfd_do_set_mode(struct net_device *ndev, enum can_mode mode)
>  {
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
>  	int ret;
>  
>  	switch (mode) {
>  	case CAN_MODE_START:
>  		loongson_canfd_reset(ndev);
>  
> +		/*
> +		 * The FIFO starts from scratch; DMA and CPU read pointer
> +		 * must be realigned with the new FIFO start.
> +		 */
> +		loongson_canfd_rxdma_stop(priv);
> +
> +		ret = loongson_canfd_rxdma_start(ndev);

[Severity: High]
Does restarting RX DMA in loongson_canfd_do_set_mode() race with concurrent
NAPI polling?

When bus-off recovery runs, priv->napi is still enabled:

CPU0                              CPU1
loongson_canfd_rx_napi()
  loongson_canfd_rx_one_frame_dma()
    saved_rd_pos = priv->rx_rd_pos;
                                  loongson_canfd_do_set_mode()
                                    loongson_canfd_reset(ndev)
                                    loongson_canfd_rxdma_stop(priv)
                                    loongson_canfd_rxdma_start(ndev)
                                      priv->rx_rd_pos = 0;
                                      priv->rx_wr_pos = 0;
    priv->rx_rd_pos = saved_rd_pos;
    <-- corrupts reset ring position

In addition, loongson_canfd_reset() resets the controller before
loongson_canfd_rxdma_stop() terminates the active DMA transfer on
LOONGSON_CANFD_RX_DATA.  In loongson_canfd_close(),
cancel_delayed_work_sync(&priv->can.restart_work) also runs before
loongson_canfd_chip_stop() disables interrupts, so a bus-off interrupt in
that window can re-queue restart_work and restart RX DMA and the chip after
they have been stopped.

[ ... ]

> @@ -1435,6 +1698,247 @@ static int loongson_canfd_rx_deliver(struct net_device *ndev,
> +static bool loongson_canfd_rx_is_empty_dma(struct loongson_canfd_priv *priv)
> +{
> +	u32 rx_stat;
> +	u32 idx, h0, h1;
> +
> +	loongson_canfd_dma_sync_wr(priv);
> +
> +	if (priv->rx_wr_pos < priv->rx_rd_pos)
> +		goto discard;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
> +
> +	/* A complete frame is waiting in the FIFO */
> +	if (FIELD_GET(REG_RX_STAT_RXFRC, rx_stat))
> +		return false;
> +
> +	/* FIFO holds a partial frame the DMA engine is still moving */
> +	if (!(rx_stat & REG_RX_STAT_RXE))
> +		return false;
> +
> +	/* Header pair not yet fully resident: let NAPI retry */
> +	if (priv->rx_wr_pos < priv->rx_rd_pos + 2)
> +		return false;

[Severity: High]
Does loongson_canfd_rx_is_empty_dma() return false when both the hardware
FIFO and the DMA ring are completely empty?

When all frames have been consumed (priv->rx_wr_pos == priv->rx_rd_pos) and
the hardware FIFO is empty, priv->rx_wr_pos < priv->rx_rd_pos + 2 is true
and this function returns false.  Every NAPI poll in DMA mode then calls
loongson_canfd_rx_one_frame_dma(), gets -EAGAIN because avail < 2, and spins
through all 100 LOONGSON_CANFD_DMA_WAIT_RETRY_MAX iterations before
completing.

Worse, if the hardware FIFO is empty while a single trailing word or
incomplete frame remains in the ring (priv->rx_wr_pos > priv->rx_rd_pos),
loongson_canfd_rx_napi() exhausts those 100 -EAGAIN retries and calls
loongson_canfd_rx_napi_done() without advancing priv->rx_rd_pos.
loongson_canfd_rx_napi_done() then sees priv->rx_wr_pos > priv->rx_rd_pos
and immediately calls napi_schedule(), causing an infinite NAPI softirq
livelock.

> +
> +	idx = priv->rx_rd_pos % LOONGSON_CANFD_RXDMA_NUM;
> +	h0 = priv->rx_cpu_buf[idx];
> +	h1 = priv->rx_cpu_buf[(idx + 1) % LOONGSON_CANFD_RXDMA_NUM];
> +
> +	/* Zero header: DMA zero-fill. Drop silently. */
> +	if (h0 == 0 && h1 == 0)
> +		goto discard;
> +
> +	/* Non-zero but implausible: mid-frame garbage. Drop silently. */
> +	if (!loongson_canfd_hdr_plausible(h0, h1))
> +		goto discard;
> +
> +	return false;
> +
> +discard:
> +	priv->rx_rd_pos = priv->rx_wr_pos;
> +	return true;
> +}

[Severity: High]
Can jumping to discard here wipe out the entire unread DMA ring buffer?

When the hardware FIFO has finished draining into the ring (RXFRC == 0 and
REG_RX_STAT_RXE set), loongson_canfd_rx_is_empty_dma() inspects the two
words at priv->rx_rd_pos before loongson_canfd_rx_one_frame_dma() can check
for a ring overrun or resynchronize across a bad header.  If the ring
overran mid-frame, or if loongson_canfd_rx_one_frame_dma_overrun() just
repositioned priv->rx_rd_pos to rx_wr_pos - LOONGSON_CANFD_RXDMA_NUM + 2
(which is usually mid-frame), !loongson_canfd_hdr_plausible(h0, h1) is true
and goto discard sets priv->rx_rd_pos = priv->rx_wr_pos, silently dropping
up to 32 KB of valid unread frames.  The same full-ring discard happens if
leading zero-fill words sit at priv->rx_rd_pos ahead of valid frames (both
here and in loongson_canfd_rx_one_frame_dma_bad_hdr()).

> +
> +static int loongson_canfd_rx_one_frame_dma_overrun(struct net_device *ndev,
> +						   u64 avail)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	struct dma_tx_state dbg_state;
> +	u32 dbg_rx_stat = 0;
> +
> +	dmaengine_tx_status(priv->rx_ch, priv->rx_cookie, &dbg_state);
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &dbg_rx_stat);
> +
> +	netdev_warn(ndev, "RX DMA overrun, resyncing\n");
> +	scoped_guard(spinlock_irqsave, &priv->stats_lock) {
> +		stats->rx_errors++;
> +		stats->rx_over_errors++;
> +		stats->rx_dropped++;
> +	}
> +
> +	/* Words older than rx_wr_pos - RXDMA_NUM are already overwritten */
> +	if (priv->rx_wr_pos > LOONGSON_CANFD_RXDMA_NUM - 2)
> +		priv->rx_rd_pos = priv->rx_wr_pos - LOONGSON_CANFD_RXDMA_NUM + 2;
> +	else
> +		priv->rx_rd_pos = 0;
> +
> +	priv->rx_bad_cnt = 0;
> +	return 0;
> +}

[Severity: High]
Can loongson_canfd_rx_one_frame_dma_overrun() trap NAPI in an unbounded
softirq loop under sustained bus load?

Setting priv->rx_rd_pos to priv->rx_wr_pos - LOONGSON_CANFD_RXDMA_NUM + 2
leaves only 2 words of headroom in the cyclic ring and uses the stale
priv->rx_wr_pos sampled before the unratelimited netdev_warn().  While
netdev_warn() runs, the cyclic DMA continues writing; as soon as 3 or more
words arrive, the next loop iteration in loongson_canfd_rx_napi() syncs
priv->rx_wr_pos and sees avail > LOONGSON_CANFD_RXDMA_NUM again.  Because
this function returns 0, work_done is never incremented in
loongson_canfd_rx_napi(), so the poll loop can repeat indefinitely while
flooding the kernel log.

> +
> +static int loongson_canfd_rx_one_frame_dma_bad_hdr(struct net_device *ndev,
> +						   const struct loongson_canfd_rx_desc *d,
> +						   u64 saved_pos)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	u32 rx_stat, tries;
> +	u64 pos;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);

[Severity: Low]
In loongson_canfd_rx_one_frame_dma_overrun(), the avail parameter,
dbg_state, and dbg_rx_stat are never used after querying DMA and MMIO
status, and in loongson_canfd_rx_one_frame_dma_bad_hdr(), rx_stat is read
from LOONGSON_CANFD_RX_STAT but never used.  Can these dead reads and
unused variables be removed?

[ ... ]

> +static int loongson_canfd_rx_one_frame_dma(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	u64 avail, saved_rd_pos = priv->rx_rd_pos;
> +	struct loongson_canfd_rx_desc d;
> +	int ret;
> +
> +	/* Guard against unsigned underflow: rd past wr means desync */
> +	if (priv->rx_wr_pos < priv->rx_rd_pos) {
> +		netdev_warn(ndev,
> +			    "RX pointer inversion: rd=%llu wr=%llu, resync\n",
> +			    priv->rx_rd_pos, priv->rx_wr_pos);
> +
> +		scoped_guard(spinlock_irqsave, &priv->stats_lock) {
> +			stats->rx_dropped++;
> +		}
> +
> +		priv->rx_rd_pos = priv->rx_wr_pos;
> +		priv->rx_bad_cnt = 0;
> +		return 0;
> +	}
> +
> +	avail = priv->rx_wr_pos - priv->rx_rd_pos;
> +
> +	if (avail > LOONGSON_CANFD_RXDMA_NUM)
> +		return loongson_canfd_rx_one_frame_dma_overrun(ndev, avail);
> +
> +	if (avail < 2)
> +		return -EAGAIN;
> +
> +	if (!loongson_canfd_rx_hdr_valid(ndev, &d))
> +		return loongson_canfd_rx_one_frame_dma_bad_hdr(ndev, &d,
> +							     saved_rd_pos);
> +
> +	priv->rx_bad_cnt = 0;
> +
> +	/*
> +	 * Re-sample the DMA position: it may have advanced while the
> +	 * header words were being read. If the payload is not fully
> +	 * resident yet, roll the read position back and let NAPI retry.
> +	 */
> +	loongson_canfd_dma_sync_wr(priv);
> +
> +	if (priv->rx_wr_pos < priv->rx_rd_pos) {
> +		priv->rx_rd_pos = priv->rx_wr_pos;
> +		return 0;
> +	}
> +
> +	if (priv->rx_wr_pos - priv->rx_rd_pos < d.rwcnt - 2) {
> +		priv->rx_rd_pos = saved_rd_pos;
> +		return -EAGAIN;
> +	}
> +
> +	ret = loongson_canfd_rx_deliver(ndev, &d);
> +	if (ret != -ENOMEM)
> +		return 1;
> +
> +	stats->rx_dropped++;

[Severity: Low]
priv->stats_lock is used to protect stats->rx_dropped++ in the pointer
inversion check above and in loongson_canfd_rx_one_frame_dma_overrun(), but
not here or in loongson_canfd_rx_one_frame_dma_bad_hdr() (and
loongson_canfd_rx_deliver() updates rx_bytes and rx_packets without
priv->stats_lock while loongson_canfd_rx_overrun_event() updates them under
priv->stats_lock in hard IRQ context).  Should ndev->stats updates use
priv->stats_lock consistently?

> +	priv->rx_rd_pos = saved_rd_pos + 1;
> +	if (++priv->rx_bad_cnt >= 64) {
> +		priv->rx_rd_pos = priv->rx_wr_pos;
> +		priv->rx_bad_cnt = 0;
> +		return -ENODATA;
> +	}
> +	return 1;
> +}

[Severity: High]
Why does the -ENOMEM path set priv->rx_rd_pos to saved_rd_pos + 1 instead
of saved_rd_pos + d.rwcnt?

At this point loongson_canfd_rx_hdr_valid() has already validated the frame
header and confirmed that all d.rwcnt words are present in the ring, and
loongson_canfd_rx_deliver() only failed to allocate an skb.  Rewinding
priv->rx_rd_pos to saved_rd_pos + 1 leaves the reader pointing at d.hdr1 in
the middle of the dropped frame, desynchronizing subsequent reads so that
header and payload words can be misinterpreted as new frame headers.

Also, because priv->rx_bad_cnt = 0 is executed unconditionally right after
loongson_canfd_rx_hdr_valid() succeeds (as well as in every other recovery
path), ++priv->rx_bad_cnt here is always 1 and the >= 64 check is
unreachable.

> @@ -1532,20 +2036,52 @@ static bool loongson_canfd_rx_overrun_event(struct net_device *ndev)
>  static void loongson_canfd_rx_napi_done(struct napi_struct *napi,
>  					bool can_complete, int work_done)
>  {
>  	struct net_device *ndev = napi->dev;
>  	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 rx_stat;
>  
>  	if (!can_complete || !napi_complete_done(napi, work_done))
>  		return;
>  
> -	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> -		     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_RBNEI));
> +	/*
> +	 * Catch a DMADI that was swallowed while SCHED was still set,
> +	 * and a stale partial header that rx_is_empty_dma() skipped.
> +	 */
> +	if (priv->rx_ch) {
> +		loongson_canfd_dma_sync_wr(priv);
> +		if (priv->rx_wr_pos > priv->rx_rd_pos) {
> +			napi_schedule(napi);
> +			return;
> +		}
> +	}
> +
> +	/*
> +	 * Flush an overrun latched while DOI was masked (e.g. during
> +	 * NAPI). DOR is sticky and the handler is idempotent, so this
> +	 * costs one regmap_read in the common case.
> +	 */
> +	loongson_canfd_rx_overrun_event(ndev);

[Severity: Medium]
Can calling loongson_canfd_rx_overrun_event() from NAPI context race with
the hard IRQ handler?

REG_INT_STAT_DOI is not masked during NAPI, only briefly inside
loongson_canfd_interrupt() while servicing DOI.  If an RX overrun occurs as
NAPI completes, both loongson_canfd_rx_napi_done() and
loongson_canfd_interrupt() can read REG_STAT_DOR before either writes
REG_CMD_CDO, double-counting rx_over_errors and rx_errors and queuing
duplicate CAN_ERR_CRTL_RX_OVERFLOW error skbs.

> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
> +
> +	if (FIELD_GET(REG_RX_STAT_RXFRC, rx_stat) == 0 &&
> +	    (rx_stat & REG_RX_STAT_RXE)) {
> +		/* Quiescent: unmask RBNEI for the next empty -> non-empty edge */
> +		regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +			     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_RBNEI));
> +		return;
> +	}

[Severity: Medium]
Can checking priv->rx_wr_pos before LOONGSON_CANFD_RX_STAT leave a DMA-moved
frame stranded in the ring with RBNEI unmasked?

If a frame is in the hardware FIFO when loongson_canfd_dma_sync_wr() runs
(so priv->rx_wr_pos == priv->rx_rd_pos), and the DMA engine finishes moving
that frame into the ring before LOONGSON_CANFD_RX_STAT is read (without
crossing a 4 KB LOONGSON_CANFD_RXDMA_PERIOD boundary to fire DMADI):

CPU0                              DMA engine
loongson_canfd_rx_napi_done()
  loongson_canfd_dma_sync_wr()
  /* rx_wr_pos == rx_rd_pos */
                                  moves frame from FIFO to ring
                                  /* FIFO is now empty */
  regmap_read(..., RX_STAT, ...)
  /* sees RXFRC == 0 && RXE */
  unmasks RBNEI
  return;                         <-- frame sits in ring unread

Because the hardware FIFO is already empty when RBNEI is unmasked and
priv->rx_wr_pos is not re-checked afterward, no interrupt fires until new
bus traffic arrives.

[ ... ]

> @@ -1847,6 +2410,10 @@ static void loongson_canfd_err_interrupt(struct net_device *ndev, u32 isr)
>   * optional HW TX timestamp) and counts it sent; TX_BS_FAIL and
>   * TX_BS_CANCEL free the skb as dropped. Clears the completion record
>   * per slot and wakes the queue if a buffer became free.
> + *
> + * RBNEI is serviced by masking it, resuming the paused RX DMA channel
> + * (no-op in poll mode) and scheduling NAPI. DMADI is never enabled, so
> + * no DMA completion IRQ is expected in the RX path.
>   */
>  static void loongson_canfd_tx_interrupt(struct net_device *ndev)

[Severity: Low]
This comment on loongson_canfd_tx_interrupt() appears to be left over from
an earlier design: it is attached to the TX interrupt handler, and in this
patch the RX DMA channel is never paused and REG_INT_STAT_DMADI is enabled
and handled in loongson_canfd_interrupt().

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1794213427.git.zhoubinbin@loongson.cn?part=2

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 1/2] can: Add Loongson CAN-FD controller driver
  2026-11-09  8:42 ` [PATCH v9 1/2] can: " Binbin Zhou
  2026-11-09  9:31   ` sashiko-bot
@ 2026-11-10 22:20   ` netdev-bot+sashiko
  1 sibling, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 22:20 UTC (permalink / raw)
  To: zhoubinbin
  Cc: zhoubb.aaron, chenhuacai, mkl, mailhol, libingxiong, chenhuacai,
	kernel, loongarch, linux-can, jeffbai, kuba

Thank you for your contribution! Sashiko AI review found 28 potential
issue(s) to consider.

Critical: 0 · High: 1 · Medium: 18 · Low: 9

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [High] Use-after-free in loongson_canfd_start_xmit()
  (drivers/net/can/loongson_canfd.c:2077-2080).
- [Medium] loongson_canfd_rx_deliver() (line 1397) sets `cfd->flags = 0;`
  after alloc_canfd_skb() has set `(*cfd)->flags = CANFD_FDF`, and only…
- [Medium] In loongson_canfd_report_state_change(), the
  CAN_STATE_ERROR_ACTIVE case (line ~1705) sets only CAN_ERR_CNT in…
- [Medium] loongson_canfd_set_btr() (line 750) only moves the excess when
  phase_seg1 > 63 (REG_BTR_PH1).
- [Medium] Teardown ordering in loongson_canfd_close() (line 2152) leaves
  a window.
- [Medium] Bus-off restart races with the TX IRQ on echo_skb[].
- [Medium] Bus-off recovery leaves the netdev in a stale state.
- [Medium] The TX path does not reject CAN XL skbs, so it reads past the
  packet data.
- [Medium] The hardirq handler loongson_canfd_interrupt() loops while
  INT_STAT != 0 for up to 10000 iterations.
- [Medium] ndev->stats is updated with inconsistent locking.
- [Medium] Several statistics are counted in the wrong place.
- [Medium] Flow control and buffer allocation disagree on what 'free'
  means.
- [Medium] Echoes can be delivered out of order.
- [Medium] Normal mode has a retransmission limit.
- [Medium] RX resynchronisation ignores the hardware's frame boundaries.
- [Medium] TXBHCI handling can lose a completion.
- [Medium] The 'lost interrupt' force-free in tx_interrupt() relies on a
  non-atomic status view.
- [Medium] The 'hardware' TX timestamps are not hardware-latched, and the
  timebase is steered toward CLOCK_REALTIME.
- [Medium] can: loongson_canfd: broken REGCACHE_MAPLE config on banked TX
  and FIFO registers
- [Low] loongson_canfd_report_state_change() increments
  can_stats.error_warning/error_passive/bus_off on every transition into…
- [Low] In loongson_canfd_tx_interrupt(), a declaration comes directly
  after a case label: `case TX_BS_VALID:` is immediately followed by…
- [Low] Hardware-timestamping state is reported inconsistently.
- [Low] loongson_canfd_hwts_setup() rewrites
  hwts->cc.read/mask/mult/shift (two separate stores in…
- [Low] loongson_canfd_tx_interrupt() decides `wake =
  loongson_canfd_txbuf_freed(priv)` under tx_lock but calls…
- [Low] loongson_canfd_set_secondary_sample_point() has a
  CAN_CTRLMODE_TDC_MANUAL branch that can never run.
- [Low] In loongson_canfd_reset() (line 727), the kernel-doc says 'The
  reset bit does not self-clear on this silicon', but the code still…
- [Low] Two comments do not match the code.
- [Low] can: loongson_canfd: tautological SFF ID check in
  loongson_canfd_hdr_plausible()

--

can: Add Loongson CAN-FD controller driver

This commit adds a driver for the CAN-FD controller in Loongson-2K SoCs.
It supports CAN 2.0B and CAN FD with NAPI RX, TX echo, error reporting
and the usual ctrlmodes. It also exposes a 16-bit free-running counter
as hardware RX/TX timestamps.

> diff --git a/drivers/net/can/loongson_canfd.c b/drivers/net/can/loongson_canfd.c
> new file mode 100644
> index 0000000000000..7a4d8df7815ac
> --- /dev/null
> +++ b/drivers/net/can/loongson_canfd.c
> @@ -0,0 +1,2414 @@

[ ... ]

> +	/* Whole windows lost: re-lock, do not slew (stamps must stay monotonic) */
> +	if (dev >= (s64)hwts->window_ns / 2 ||
> +	    dev <= -(s64)hwts->window_ns / 2) {
> +		timecounter_init(&hwts->tc, &hwts->cc, ktime_get_real_ns());
> +		return;
> +	}
> +
> +	if (dev > -LOONGSON_CANFD_HWTS_SLEW_BAND_NS &&
> +	    dev < LOONGSON_CANFD_HWTS_SLEW_BAND_NS)
> +		return;
> +
> +	timecounter_adjtime(&hwts->tc, dev >> LOONGSON_CANFD_HWTS_SLEW_SHIFT);
> +}

[ ... ]

> +static ktime_t loongson_canfd_hwts_now(struct loongson_canfd_priv *priv)
> +{
> +	struct loongson_canfd_hwts *hwts = &priv->hwts;
> +
> +	/* Sample first: the counter is the earlier of the two instants */
> +	return loongson_canfd_hwts_ts(priv, (u32)hwts->cc.read(&hwts->cc));
> +}

[Severity: Medium]
The commit message says the driver "exposes the counter as hardware RX/TX
timestamps". loongson_canfd_get_ts_info() also advertises
SOF_TIMESTAMPING_TX_HARDWARE and SOF_TIMESTAMPING_RAW_HARDWARE.

Is that accurate for TX? loongson_canfd_tx_interrupt() takes its stamp
from loongson_canfd_hwts_now(), which reads the free-running counter live
at whatever moment the ISR reaches the buffer. That reading includes IRQ
latency and the time spent scanning the other buffers. The comment in
tx_interrupt() itself says the controller latches no per-frame TX stamp.

Also, loongson_canfd_hwts_anchor() slews tc.nsec by a signed dev >> 3 and
re-initializes it from ktime_get_real_ns() on a half-window deviation.
Can the reported raw hardware timebase step backwards here, even though
the comment above says stamps must stay monotonic?

Should the commit message mention these limitations?

> +	hwts->cc.read = loongson_canfd_hwts_read_cycles;
> +	hwts->cc.mask = CYCLECOUNTER_MASK(LOONGSON_CANFD_HWTS_COUNTER_BITS);
> +	clocks_calc_mult_shift(&hwts->cc.mult, &hwts->cc.shift,
> +			       tick_hz, NSEC_PER_SEC, 60);
> +
> +	hwts->window_ns = cyclecounter_cyc2ns(&hwts->cc,
> +					      hwts->cc.mask + 1, 0, &frac);

[Severity: Low]
These fields, along with tick_ns and psc above, are written outside
hwts->lock. Only timecounter_init() runs under the lock.

On the bus-off restart path, can_restart()->do_set_mode()->chip_start()->
hwts_setup(), NAPI or the TX IRQ may already have passed the
READ_ONCE(hwts.ready) check. They can then be inside
loongson_canfd_hwts_ts() reading tc.cc->mult, shift and window_ns under
the lock.

Is this a data race on state that is documented as protected by
hwts->lock? On restart the values are usually rewritten with the same
numbers, so in practice this would mostly show up under KCSAN.

> +/**
> + * loongson_canfd_reset() - Issue soft reset to controller
> + * @ndev: Network device
> + *
> + * The reset bit does not self-clear on this silicon; the mode write
> + * below re-programs the register, so a timeout is only logged.
> + */

[ ... ]

> +	ret = regmap_read_poll_timeout(priv->regmap, LOONGSON_CANFD_MODE,
> +				       val, !(val & REG_MODE_RST),
> +				       10, 10 * USEC_PER_MSEC);

[Severity: Low]
The kernel-doc says the reset bit does not self-clear, but this still
polls for up to 10ms waiting for it to clear.

If the comment is right, doesn't every open() and every CAN_MODE_START
restart busy-wait the full 10ms? If the bit does clear, should the
comment be updated?

[ ... ]

> +	/* PROP is 7 bits but PH1 only 6; move the excess into PROP */
> +	if (phase1 > ph1_max) {
> +		prop += phase1 - ph1_max;
> +		phase1 = ph1_max;
> +	}
> +
> +	if (nominal) {
> +		btr = FIELD_PREP(REG_BTR_PROP, prop) |

[Severity: Medium]
What happens when prop_seg alone is larger than REG_BTR_PROP
(GENMASK(6, 0), so at most 127)?

Both bittiming_const tables use tseg1_max = 190. can_fixup_bittiming()
only checks prop_seg + phase_seg1 against tseg1_max. A manual setting
such as "prop-seg 150 phase-seg1 10" is therefore accepted, and
FIELD_PREP(REG_BTR_PROP, 150) masks it to 22 at runtime.

Would the controller then run with a different bit timing than the one
reported to userspace? can_calc_bittiming() never produces these values,
so this needs a manual setting.

[ ... ]

> +	if (ctrlmode & CAN_CTRLMODE_TDC_MANUAL)
> +		/* Manual TDC: 2'd2, delay = SSP_OFF */
> +		ssp_cfg = FIELD_PREP(REG_SSP_CFG_SRC, 2) |
> +			  FIELD_PREP(REG_SSP_CFG_OFF, priv->can.fd.tdc.tdco);

[Severity: Low]
This isn't a bug, but can this branch ever run?

ctrlmode_supported in loongson_canfd_setup_can_priv() includes only
CAN_CTRLMODE_TDC_AUTO, and the netlink code rejects unsupported ctrlmode
bits. In addition, can_calc_tdco() only ever sets TDC_AUTO, and tdc_const
has tdcv_min = tdcv_max = 0.

[ ... ]

> +/**
> + * loongson_canfd_get_auto_tdcv() - Get transmitter delay compensation value
> + * @ndev: Network device
> + * @tdcv: Pointer to store TDCV value
> + *
> + * Return: 0 on success, -EOPNOTSUPP on CAN_CTRLMODE_TDC_AUTO is not set.
> + */

[ ... ]

> +	/* Hardware measurement is complete only when TRV_DLY is not 0 */
> +	if (!*tdcv)
> +		return -EAGAIN;

[Severity: Low]
This isn't a bug, but the kernel-doc does not list the -EAGAIN return.

The TXBHCI comment in loongson_canfd_interrupt() has a similar mismatch.
It refers to "the W1C in tx_interrupt()", but the INT_STAT write is in
the ISR right before the call, not in loongson_canfd_tx_interrupt().

In the final patch of the series, the kernel-doc added to tx_interrupt()
also says "DMADI is never enabled", but chip_start() enables DMADI when
rx_ch exists.

[ ... ]

> +	/* RTRLE always on; ONE_SHOT only decides RTRTH */
> +	conf |= REG_CONF_RTRLE;
> +	conf |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_LOOPBACK,
> +					REG_CONF_ILBP);
> +	conf |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_FD_NON_ISO,
> +					REG_CONF_NISOFD);
> +	conf |= LOONGSON_CANFD_FIELD_IF(!(ctrlmode & CAN_CTRLMODE_ONE_SHOT),
> +					FIELD_PREP(REG_CONF_RTRTH,
> +						   LOONGSON_CANFD_MAX_RTXTH));

[Severity: Medium]
With the retransmission limit always enabled, doesn't normal (not
one-shot) mode give up after about 16 attempts and mark the buffer
TX_BS_FAIL? loongson_canfd_tx_interrupt() then frees the echo skb and
counts it in tx_dropped.

Without CAN_CTRLMODE_ONE_SHOT, the expected behaviour is unlimited
retransmission until success or bus-off. ctucanfd clears RTRLE in that
case.

A node with no ACK partner would silently lose frames well before it
reaches bus-off. So would a low-priority frame on a busy bus, if
arbitration loss counts as an attempt.

[ ... ]

> +	/* Need both BRP clear and BS idle; skip slots still echoing */
> +	for_each_clear_bit(i, &tx_brp, LOONGSON_CANFD_TXBUF_NUM) {
> +		u32 mask = 0x3 << (i * 2);
> +
> +		if (!(tx_bs & mask)) {
> +			if (priv->can.echo_skb[i])
> +				continue;
> +			return i;
> +		}
> +	}

[Severity: Medium]
Can frames be echoed, or even sent, in a different order from the one
they were submitted in?

This always picks the lowest-index free buffer, and
loongson_canfd_tx_interrupt() handles completions in index order 0..7.
Suppose frame A is in buffer 5 and a later frame B goes into buffer 0. If
both complete before one ISR pass, B is echoed before A.

The driver also programs no TX priority or ordering control (only BUFM at
reset), so the order on the wire may differ too.

The CAN stack expects in-order TX and echo; RECV_OWN_MSGS users such as
ISO-TP depend on it. ctucanfd rotates buffer priorities and tracks
head/tail indices for this reason.

[ ... ]

> +	words = rwcnt > 2 ?
> +		min_t(u32, rwcnt - 2,
> +		      CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE) : 0;
> +	while (words--)
> +		regmap_read(priv->regmap, LOONGSON_CANFD_RX_DATA, &data);

[Severity: Medium]
Does this drain stay inside the rejected frame?

When loongson_canfd_rx_hdr_valid() sees RWCNT < 2, it substitutes the
worst-case length of 2 + 16. This loop then reads 16 words no matter what
the 80-byte FIFO actually holds. With RXBAM auto-advance, these reads
could run into the next frames and leave the cursor in the middle of a
frame.

After that, every later "header" would really be payload. Each one would
be rejected and trigger another fixed drain. A payload word pair that
happens to pass loongson_canfd_hdr_plausible() would be delivered as a
bogus frame.

REG_RX_STAT_RXMOF and REG_CMD_RRB are defined but never used. Would a
FIFO flush be a safer way to resync?

Also, loongson_canfd_rx_is_empty_fifo() treats RXFRC == 0 with RXE == 0
as non-empty. That means reading can start before any complete frame has
been committed. Is that intended? The FIFO mode code is unchanged in the
final patch of the series.

[ ... ]

> +	cfd->flags = 0;
> +
> +	if (d->is_can_fd) {
> +		if (d->hdr1 & REG_FRAME_META1_BRS)
> +			cfd->flags |= CANFD_BRS;
> +		if (d->hdr0 & REG_FRAME_META0_ESI)
> +			cfd->flags |= CANFD_ESI;

[Severity: Medium]
Doesn't this clear the CANFD_FDF flag that alloc_canfd_skb() sets?

include/uapi/linux/can.h says CANFD_FDF is set in all CAN FD frame
structures provided by the CAN subsystem. With this assignment, every FD
frame received through this driver reaches userspace without it.

[ ... ]

> +	if (!d->is_rtr)
> +		stats->rx_bytes += cfd->len;
> +	stats->rx_packets++;

[Severity: Medium]
stats_lock is documented as protecting ndev->stats.
loongson_canfd_rx_overrun_event() (hardirq) and
loongson_canfd_report_bus_error() update rx_packets, rx_bytes, rx_errors
and rx_over_errors under it.

Here in NAPI, the same counters are updated without the lock and with
IRQs enabled. loongson_canfd_rx_note_bad() does the same for rx_dropped,
and so does rx_one_frame_dma*() in the final patch.

Can a DOI interrupt that lands in the middle of this read-modify-write
lose increments? As written, the lock does not seem to protect anything.

[ ... ]

> +	priv->can.state = new_state;
> +
> +	/* Stats and BUS_OFF handling must not depend on skb allocation */
> +	switch (new_state) {
> +	case CAN_STATE_BUS_OFF:
> +		priv->can.can_stats.bus_off++;
> +		can_bus_off(ndev);
> +		break;

[Severity: Medium]
Can the bus-off restart race with the TX IRQ on echo_skb[]?

On bus-off only can_bus_off() is called. CONF_ENA, INT_ENA, NAPI and the
IRQ all stay active, and armed TX buffers are not cancelled.

When restart_ms is set, can_restart() calls can_flush_echo_skb() without
tx_lock. It relies on this comment in drivers/net/can/dev/dev.c:

	/* No synchronization needed because the device is bus-off and
	 * no messages can come in or go out.
	 */

The controller might report TX completions at that moment: FAIL or
CANCEL, or VALID after automatic recovery. In that case
loongson_canfd_tx_interrupt() on another CPU can call can_get_echo_skb()
or can_free_echo_skb() on the same slot.

Could that free an echo skb twice, or pass an already freed skb to
netif_rx()?

do_set_mode()->reset()/chip_start() also rewrites the MODE, CONF and INT
registers and can.state without tx_lock. xmit, NAPI or err_interrupt may
be running at the same time. Whether TXBHCI fires while bus-off depends
on the hardware.

> +	case CAN_STATE_ERROR_PASSIVE:
> +		priv->can.can_stats.error_passive++;
> +		break;
> +	case CAN_STATE_ERROR_WARNING:
> +		priv->can.can_stats.error_warning++;
> +		break;

[Severity: Low]
These counters are incremented on every transition into the state,
including improvements such as error-passive to error-warning. The
core's can_update_state_error_stats() counts only escalations:

	if (new_state <= priv->state)
		return;

Should this driver use the same semantics, for example by using
can_change_state()? As written, the IFLA_INFO_XSTATS counters mean
something different for this driver than for others.

[ ... ]

> +	case CAN_STATE_ERROR_ACTIVE:
> +		cf->can_id |= CAN_ERR_CNT;
> +		cf->data[1] = CAN_ERR_CRTL_ACTIVE;
> +		break;

[Severity: Medium]
data[1] only carries meaning when CAN_ERR_CRTL is set in can_id. Should
this be CAN_ERR_CRTL | CAN_ERR_CNT, the way can_change_state() does it?
Without the class bit, decoders that check it never see the return to
error-active.

The same function also counts transitions into warning, passive and
bus-off that are improvements, as described in the previous comment.

[ ... ]

> +	if (isr & (REG_INT_STAT_FCSI | REG_INT_STAT_EWLI)) {
> +		mask |= REG_INT_STAT_FCSI | REG_INT_STAT_EWLI;
> +		state = loongson_canfd_get_fault_state(ndev);
> +
> +		/*
> +		 * FCSI is latched on reset -> Error Active, but that is
> +		 * not a protocol-visible transition: filter it out.
> +		 */
> +		if (priv->can.state != state)
> +			loongson_canfd_report_state_change(ndev, state, &bec);
> +	}

[Severity: Medium]
What happens on the transition out of BUS_OFF?

Suppose the controller recovers automatically and raises FCSI.
loongson_canfd_report_state_change() then overwrites priv->can.state with
ACTIVE, WARNING or PASSIVE. However, carrier is never turned back on and
the queue is not woken; the driver has no netif_carrier_on() anywhere.

With restart-ms=0, the hardware may be back on the bus sending
still-armed buffers while the interface keeps carrier off. A manual
restart would then fail with -EBUSY, because can_restart_now() checks:

	if (priv->state != CAN_STATE_BUS_OFF)
		return -EBUSY;

Is down/up the only way to recover here?

[ ... ]

> +	scoped_guard(spinlock_irqsave, &priv->tx_lock) {
> +		regmap_read(priv->regmap, LOONGSON_CANFD_TX_STAT, &tx_stat);
> +
> +		for (unsigned int buf_id = 0; buf_id < LOONGSON_CANFD_TXBUF_NUM; buf_id++) {
> +			sts = loongson_canfd_get_txbuf_sts(priv, buf_id);
> +
> +			if (sts == TX_BS_IDLE) {
> +				/* BRP set: hardware still queued/arbitrating */
> +				if (tx_stat & BIT(buf_id))
> +					continue;
> +
> +				/*
> +				 * Hardware is idle but echo_skb is occupied
> +				 * (lost interrupt). Free it, but do not issue
> +				 * SR_CLEAN on an idle slot: it can fake a
> +				 * command interrupt and cause a storm.
> +				 */
> +				if (priv->can.echo_skb[buf_id]) {
> +					netdev_warn(ndev,
> +						    "TX buf %u idle but echo_skb occupied, force free\n",
> +						    buf_id);
> +					can_free_echo_skb(ndev, buf_id, NULL);
> +					stats->tx_dropped++;
> +				}
> +				continue;
> +			}

[Severity: Medium]
BRP comes from the tx_stat snapshot taken before the loop. BS comes from
a separate TX_STAT read inside loongson_canfd_get_txbuf_sts().

The hardware may clear BRP once arbitration is won and update BS only at
the end of the frame. Could a scan during that frame then see BRP clear
and BS idle, and free the echo of a frame that is still on the wire? Such
a scan could be triggered by the previous buffer's completion in a
back-to-back burst.

loongson_canfd_find_free_txbuf() would then hand out the slot.
loongson_canfd_start_xmit() could rewrite that buffer through TX_SEL and
TX_DATA while it is still being transmitted.

> +
> +			processed = true;
> +			switch (sts) {
> +			case TX_BS_VALID:
> +				u32 dummy_len;

[Severity: Low]
Does this build with older compilers? GCC before 11 rejects a
declaration directly after a case label:

  a label can only be part of a statement and a declaration is not a
  statement

Documentation/process/changes.rst lists GCC 8.1 as the minimum, and
CONFIG_CAN_LOONGSON_CANFD depends only on HAS_IOMEM. An allmodconfig
build with GCC 8 to 10 on any architecture would fail here. The final
patch in the series does not change this code. Adding braces around the
case body, or moving the declaration up, would avoid the error.

[ ... ]

> +				dummy_len = can_get_echo_skb(ndev, buf_id, NULL);
> +				(void)dummy_len;
> +				stats->tx_packets++;
> +				break;

[ ... ]

> +		wake = loongson_canfd_txbuf_freed(priv);
> +	}
> +
> +	if (wake)
> +		netif_wake_queue(ndev);
> +}

[Severity: Low]
wake is decided under tx_lock but acted on after the lock is dropped.

In between, loongson_canfd_start_xmit() on another CPU can fill the last
free buffer and call netif_stop_queue(). This would then wake a full
queue, and the next xmit would hit the "No free TX buffer while queue
was awake!" NETDEV_TX_BUSY path. The next completion recovers the queue.

[ ... ]

> +		if (isr & REG_INT_STAT_TXBHCI) {
> +			/*
> +			 * Mask-clear-handle-unmask: a TX completing during
> +			 * handling re-latches TXBHCI and fires a fresh IRQ
> +			 * after the unmask, instead of being wiped by the
> +			 * W1C in tx_interrupt().
> +			 */
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +				     REG_INT_STAT_TXBHCI);
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
> +				     REG_INT_STAT_TXBHCI);
> +			loongson_canfd_tx_interrupt(ndev);
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +				     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_TXBHCI));
> +		}

[Severity: Medium]
Does a masked source still latch into INT_STAT on this controller? The
comment depends on that. On CTU CAN FD style IP, which this register
layout resembles, masked interrupts are not captured.

If the same is true here, consider a buffer that completes after
tx_interrupt() has read its BS field but before the unmask. It would
raise no new interrupt. Its echo_skb would stay allocated, and
loongson_canfd_find_free_txbuf() would skip the slot until some unrelated
completion triggers another scan.

If that buffer held the last frame of a burst, could the echo and the
slot stay stuck indefinitely? The DOI path below has the same window.

[ ... ]

> +	netdev_err(ndev, "Interrupt loop timeout, state: 0x%x\n", isr);
> +
> +	if (isr & REG_INT_STAT_TXBHCI) {

[ ... ]

> +	loongson_canfd_disable_interrupts(priv);
> +
> +	return IRQ_HANDLED;
> +}

[Severity: Medium]
The loop in loongson_canfd_interrupt() keeps going while INT_STAT is
non-zero, for up to 10000 iterations. Each iteration does many MMIO
accesses and may allocate error skbs.

loongson_canfd_err_interrupt() unmasks BEI and ALI again on every pass.
With berr-reporting enabled, could a steady stream of bus errors keep
this hardirq loop running up to the limit?

Only RBNEI, TXBHCI, DOI and ERRORI are acknowledged (plus DMADI in the
final patch), so any other INT_STAT bit that is set would also spin to
the limit. loongson_canfd_chip_start() briefly unmasks every source with
REG_INT_MASK_CLR and never W1C-clears stale INT_STAT bits.

On timeout, only the interrupts are disabled. CONF_ENA stays set,
can.state, carrier and the queue are unchanged, and no recovery is
scheduled. Doesn't that leave the controller on the bus with RX and TX
completions never serviced again until ifdown/ifup?

[ ... ]

> +	if (can_dropped_invalid_skb(ndev, skb))
> +		return NETDEV_TX_OK;

[Severity: Medium]
Should this be can_dev_dropped_skb()? can_dropped_invalid_skb() accepts
valid ETH_P_CANXL skbs, and it does not enforce listen-only mode.

loongson_canfd_insert_frame() treats every skb that is not CAN FD as
Classic CAN. For a CAN XL frame, cf->len is really canxl_frame.flags,
which is always >= 0x80 because CANXL_XLF is set. That value then bounds
this loop:

	for (unsigned int i = 0; i < cf->len; i += LOONGSON_CANFD_DW_BYTE) {

The loop reads get_unaligned_le32(cf->data + i) far past the packet data
(skb->len can be as small as 13). It stops only when the regmap write
beyond max_register fails.

That failure then hits the netdev_err() below, which is not rate-limited,
once per packet. A CAP_NET_RAW user can inject such frames through
PF_PACKET. The buffer is never armed, so nothing reaches the bus.

> +
> +	guard(spinlock_irqsave)(&priv->tx_lock);
> +
> +	if (unlikely(!loongson_canfd_txbuf_freed(priv))) {
> +		netif_stop_queue(ndev);
> +		netdev_dbg(ndev, "No free TX buffer while queue was awake!\n");
> +		return NETDEV_TX_BUSY;
> +	}
> +
> +	buf_id = loongson_canfd_find_free_txbuf(priv);
> +	if (buf_id < 0) {
> +		netif_stop_queue(ndev);
> +		netdev_dbg(ndev, "No writable TX buffer, stopping queue\n");
> +		return NETDEV_TX_BUSY;
> +	}

[Severity: Medium]
loongson_canfd_txbuf_freed() treats any buffer with BRP clear as free.
loongson_canfd_find_free_txbuf() also requires BS to be idle and
echo_skb[] to be empty. The queue is stopped here, and woken in
tx_interrupt(), based on the weaker test.

Take a buffer that has just finished: BRP is clear, BS is VALID or FAIL,
and echo_skb is still held until the ISR cleans it up. In that window,
can xmit pass the first check and then return NETDEV_TX_BUSY from the
second one?

Should both checks use the same predicate?

[ ... ]

> +	can_put_echo_skb(skb, ndev, buf_id, 0);
> +
> +	if (!(cf->can_id & CAN_RTR_FLAG))
> +		stats->tx_bytes += cf->len;

[Severity: High]
Can this read freed memory? cf points into skb->data, and
can_put_echo_skb() always consumes the skb. If skb_clone() fails in
can_create_echo_skb():

	nskb = skb_clone(skb, GFP_ATOMIC);
	if (unlikely(!nskb)) {
		kfree_skb(skb);
		return NULL;
	}

can_put_echo_skb() returns -ENOMEM, which is ignored here. cf->can_id
and cf->len are then read from the freed skb.

IFF_ECHO is always set by this driver, so every transmit goes through
the clone path. The final patch in the series does not change this code.

> +
> +	ret = loongson_canfd_set_txbuf_cmd(ndev, TXT_CMD_ADD, buf_id);
> +	if (ret) {
> +		netdev_err(ndev, "Failed to start TX buffer %d: %d\n",
> +			   buf_id, ret);
> +		can_free_echo_skb(ndev, buf_id, NULL);
> +		ndev->stats.tx_dropped++;
> +		return NETDEV_TX_OK;
> +	}

[Severity: Medium]
tx_bytes is counted at submit time but never taken back. That covers
this failure path and buffers that later end in TX_BS_FAIL or
TX_BS_CANCEL, where only tx_dropped is incremented. tx_packets, on the
other hand, is counted only on TX_BS_VALID.

The length returned by can_get_echo_skb() in
loongson_canfd_tx_interrupt() is discarded into dummy_len. Would
counting tx_bytes there keep tx_bytes and tx_packets consistent?

Separately, loongson_canfd_rx_overrun_event() counts the locally
generated CAN_ERR_CRTL error frame in rx_packets and rx_bytes. CAN
drivers no longer count error frames there.

[ ... ]

> +	netif_stop_queue(ndev);
> +	napi_disable(&priv->napi);
> +
> +	/*
> +	 * Cancel a pending bus-off restart before tearing down IRQ/DMA:
> +	 * do_set_mode() re-arms both, so a restart firing after
> +	 * free_irq() would touch released resources. close_candev()
> +	 * does the same but only after free_irq().
> +	 */
> +	cancel_delayed_work_sync(&priv->can.restart_work);
> +
> +	loongson_canfd_chip_stop(ndev);
> +	free_irq(ndev->irq, ndev);
> +	close_candev(ndev);

[Severity: Medium]
Can restart_work be armed again after this cancel? At this point the IRQ
is still registered and enabled.

A bus-off FCSI could arrive before loongson_canfd_chip_stop(). An ISR
could also still be running on another CPU, since there is no
synchronize_irq(). Either one goes through:

err_interrupt()->report_state_change()->can_bus_off()

can_bus_off() schedules restart_work when restart_ms is set.

If that work fires during chip_stop() or free_irq(), can_restart()->
loongson_canfd_do_set_mode(CAN_MODE_START) resets the chip and
re-enables INT_ENA, INT_MASK and CONF_ENA. In the final version of the
series it also restarts the RX DMA. All of this happens after ndo_stop,
with no handler on the IRQF_SHARED line. The later cancel in
close_candev() only waits for a work item that is already running.

Could the unhandled interrupts on the shared line then make the "nobody
cared" logic disable it for the other devices on that line?

[ ... ]

> +	if (READ_ONCE(priv->hwts.ready)) {
> +		info->so_timestamping |= SOF_TIMESTAMPING_TX_HARDWARE |
> +					 SOF_TIMESTAMPING_RX_HARDWARE |
> +					 SOF_TIMESTAMPING_RAW_HARDWARE;
> +		info->tx_types = BIT(HWTSTAMP_TX_ON);
> +		info->rx_filters = BIT(HWTSTAMP_FILTER_ALL);
> +	}

[Severity: Low]
Hardware timestamping is advertised here only while hwts.ready is set.
However, ndo_hwtstamp_get/set use can_hwtstamp_get() and
can_hwtstamp_set(), which always report and accept HWTSTAMP_TX_ON and
HWTSTAMP_FILTER_ALL. As a result, ethtool -T and SIOCGHWTSTAMP can
disagree.

hwts.ready is false when the interface is down or when
loongson_canfd_hwts_setup() failed. In that state, can_put_echo_skb()
still sets SKBTX_IN_PROGRESS, and __can_get_echo_skb() still calls
skb_tstamp_tx() with skb_hwtstamps(skb). Wouldn't userspace then receive
a hardware TX timestamp of zero?

[ ... ]

> @@ -0,0 +1,2414 @@
> +// SPDX-License-Identifier: GPL-2.0+
> +/*
> + * LOONGSON CANFD controller driver
> + *
> + * Copyright (C) 2024-2026 Loongson Technology Corporation Limited
> + */
> +
> +#include <linux/acpi.h>
> +#include <linux/bitfield.h>
> +#include <linux/bits.h>
> +#include <linux/can/dev.h>
> +#include <linux/can/error.h>
> +#include <linux/cleanup.h>
> +#include <linux/clocksource.h>
> +#include <linux/ethtool.h>
> +#include <linux/interrupt.h>
> +#include <linux/module.h>
> +#include <linux/netdevice.h>
> +#include <linux/net_tstamp.h>
> +#include <linux/platform_device.h>
> +#include <linux/regmap.h>
> +#include <linux/skbuff.h>
> +#include <linux/string.h>
> +#include <linux/timecounter.h>
> +#include <linux/timekeeping.h>
> +#include <linux/types.h>
> +#include <linux/unaligned.h>
> +
> +#define LOONGSON_CANFD_DEVICE_ID	0x0	/* CANFD controller ID Register */
> +#define LOONGSON_CANFD_MODE		0x4	/* Mode Configuration Register */
> +#define LOONGSON_CANFD_CONF		0x8	/* Configure Register */
> +#define LOONGSON_CANFD_STAT		0xc	/* Status Register */
> +#define LOONGSON_CANFD_CMD		0x10	/* Command Register */
> +#define LOONGSON_CANFD_INT_STAT		0x14	/* Interrupt Status Register */
> +#define LOONGSON_CANFD_INT_ENA		0x18	/* Interrupt Enable Register */
> +#define LOONGSON_CANFD_INT_MASK		0x1c	/* Interrupt Mask Register */
> +#define LOONGSON_CANFD_BTR_NORM		0x20	/* Normal Rate Configuration Register */
> +#define LOONGSON_CANFD_BTR_FD		0x24	/* FD Data Rate Configuration Register */
> +#define LOONGSON_CANFD_ERL		0x28	/* Error Threshold Configuration Register */
> +#define LOONGSON_CANFD_FSTAT		0x2c	/* Error Status Register */
> +#define LOONGSON_CANFD_ERC		0x30	/* Error Count Register */
> +#define LOONGSON_CANFD_BRE		0x34	/* Rate Error Count Register */
> +#define LOONGSON_CANFD_CTR_PRES		0x38	/* Error Count Debug Register */
> +#define LOONGSON_CANFD_ERR_CAPT		0x3c	/* Error Capture Status Register */
> +#define LOONGSON_CANFD_RETX_CNT		0x40	/* Retransmission Count Register */
> +#define LOONGSON_CANFD_ALC		0x44	/* Lost Arbitration Capture Register */
> +#define LOONGSON_CANFD_TRV_DLY		0x48	/* Transmission Delay Measurement Register */
> +#define LOONGSON_CANFD_SSP_CFG		0x4c	/* Second Sampling Point Configuration Register */
> +#define LOONGSON_CANFD_RX_FR_CNT	0x50	/* Receive Message Count Register */
> +#define LOONGSON_CANFD_TX_FR_CNT	0x54	/* Transmit Message Count Register */
> +#define LOONGSON_CANFD_DEBUG		0x58	/* Debug Register */
> +#define LOONGSON_CANFD_TS		0x5c	/* Timestamp Register */
> +#define LOONGSON_CANFD_TX_FRM_TST	0x60	/* Transmit Message Debug Register */
> +#define LOONGSON_CANFD_FRC_DIV		0x64	/* Fractional Divider Ratio Register */
> +#define LOONGSON_CANFD_FLT_A_MASK	0x68	/* Filter A Mask Register */
> +#define LOONGSON_CANFD_FLT_A_VAL	0x6c	/* Filter A value Register */
> +#define LOONGSON_CANFD_FLT_B_MASK	0x70	/* Filter B Mask Register */
> +#define LOONGSON_CANFD_FLT_B_VAL	0x74	/* Filter B value Register */
> +#define LOONGSON_CANFD_FLT_C_MASK	0x78	/* Filter C Mask Register */
> +#define LOONGSON_CANFD_FLT_C_VAL	0x7c	/* Filter C value Register */
> +#define LOONGSON_CANFD_FLT_R_LOW	0x80	/* Range Filter Low Threshold Register */
> +#define LOONGSON_CANFD_FLT_R_HI		0x84	/* Range Filter High Threshold Register */
> +#define LOONGSON_CANFD_FLT_CTRL		0x88	/* Filter Control Register */
> +#define LOONGSON_CANFD_RX_MEM_INFO	0x8c	/* Receive Buffer Information Register */
> +#define LOONGSON_CANFD_RX_PRT		0x90	/* Receive Buffer Pointer Register */
> +#define LOONGSON_CANFD_RX_STAT		0x94	/* Receive Buffer Status Register */
> +#define LOONGSON_CANFD_RX_DATA		0x98	/* Receive Data Register */
> +#define LOONGSON_CANFD_TX_STAT		0x9c	/* Transmit Buffer Status Register */
> +#define LOONGSON_CANFD_TX_CMD		0xa0	/* Transmit Command Register */
> +#define LOONGSON_CANFD_TX_SEL		0xa4	/* Transmit Buffer Selection Register */
> +#define LOONGSON_CANFD_TX_DATA_1	0xb0
> +#define LOONGSON_CANFD_TX_DATA_18	0xf4
> +
> +/* Bitfields of CANFD controller ID register */
> +#define REG_ID_MASK			GENMASK(15, 0)
> +#define REG_ID_VER_MIN			GENMASK(23, 16)
> +#define REG_ID_VER_MAJ			GENMASK(31, 24)
> +
> +/* Bitfields of Mode Configuration register */
> +#define REG_MODE_RST			BIT(0)	/* Soft Reset */
> +#define REG_MODE_BMM			BIT(1)	/* Bus Monitoring Mode */
> +#define REG_MODE_STM			BIT(2)	/* Self-Test Mode */
> +#define REG_MODE_AFM			BIT(3)	/* Receive Filter Mode */
> +#define REG_MODE_FDE			BIT(4)	/* CANFD Enabled */
> +#define REG_MODE_TTTM			BIT(5)	/* Timed Transmission Mode */
> +#define REG_MODE_ROM			BIT(6)	/* Restricted Operation Mode */
> +#define REG_MODE_ACF			BIT(7)	/* No ACK Mode*/
> +#define REG_MODE_TSTM			BIT(8)	/* Test Mode */
> +#define REG_MODE_RXBAM			BIT(9)	/* Receive Buffer Auto Mode */
> +#define REG_MODE_ITSM			BIT(10)	/* Internal Timestamp Mode */
> +#define REG_MODE_RTSOP			BIT(12)	/* Receive Buffer Timestamp Addition Mode */
> +#define REG_MODE_BUFM			BIT(13)	/* Transmit Buffer Mode */
> +
> +/* Bitfields of Configure register */
> +#define REG_CONF_RTRLE			BIT(0)	/* Retransmission Threshold Enable */
> +#define REG_CONF_RTRTH			GENMASK(4, 1)	/* Retransmission Threshold */
> +#define REG_CONF_ILBP			BIT(5)	/* Internal Loopback Mode */
> +#define REG_CONF_ENA			BIT(6)	/* Controller Enable */
> +#define REG_CONF_NISOFD			BIT(7)	/* NON-ISO CANFD Protocol Enable */
> +#define REG_CONF_PEX			BIT(8)	/* Protocol Exception Handling */
> +#define REG_CONF_FDRF			BIT(10)	/* Ignore Remote Frame Enable */
> +
> +/* Bitfields of Status register */
> +#define REG_STAT_RXNE			BIT(0)	/* Receive Buffer Not Empty Status */
> +#define REG_STAT_DOR			BIT(1)	/* Receive Data Overflow Status */
> +#define REG_STAT_EFT			BIT(3)	/* Error Frame Flag */
> +#define REG_STAT_RXS			BIT(4)	/* Receive Status Flag */
> +#define REG_STAT_TXS			BIT(5)	/* Transmission Status Flag */
> +#define REG_STAT_EWL			BIT(6)	/* Transmission Error Threshold Flag */
> +#define REG_STAT_IDLE			BIT(7)	/* Bus Idle Flag */
> +#define REG_STAT_PEXS			BIT(8)	/* Protocol Exception Status */
> +#define REG_STAT_STCNT			BIT(16)	/* Transmission Count Support */
> +
> +/* Bitfields of Command register */
> +#define REG_CMD_RXRPMV			BIT(1)	/* Receive Buffer Read-to-Move */
> +#define REG_CMD_RRB			BIT(2)	/* Receive Buffer Release */
> +#define REG_CMD_CDO			BIT(3)	/* Received Data Overflow Status Clear */
> +#define REG_CMD_ERCRST			BIT(4)	/* Error Count Clear */
> +#define REG_CMD_RXFCRST			BIT(5)	/* Receive Frame Count Clear */
> +#define REG_CMD_TXFCRST			BIT(6)	/* Transmit Frame Count Clear */
> +#define REG_CMD_CPEXS			BIT(7)	/* Exception Status Clear */
> +
> +/* Bitfields of Interrupt Status register */
> +#define REG_INT_STAT_RXI		BIT(0)	/* Message Reception Interrupt */
> +#define REG_INT_STAT_TXI		BIT(1)	/* Message Transmission Interrupt */
> +#define REG_INT_STAT_EWLI		BIT(2)	/* Error Warning Threshold Interrupt */
> +#define REG_INT_STAT_DOI		BIT(3)	/* Receive Data Overflow Interrupt */
> +#define REG_INT_STAT_FCSI		BIT(4)	/* Fault State Change Interrupt */
> +#define REG_INT_STAT_ALI		BIT(5)	/* Arbitration Loss Interrupt */
> +#define REG_INT_STAT_BEI		BIT(6)	/* Bus Error Interrupt */
> +#define REG_INT_STAT_RXFI		BIT(7)	/* Receive Buffer Full Interrupt */
> +#define REG_INT_STAT_BSI		BIT(8)	/* Rate Switching Interrupt */
> +#define REG_INT_STAT_RBNEI		BIT(9)	/* Receive Buffer Not Empty Interrupt */
> +#define REG_INT_STAT_TXBHCI		BIT(10)	/* Transmit Buffer Hardware Command Interrupt */
> +#define REG_INT_STAT_OFI		BIT(11)	/* Overload Frame Transmission Interrupt */
> +#define REG_INT_STAT_DMADI		BIT(12)	/* Message DMA Transfer Complete Interrupt */
> +
> +#define REG_INT_STAT_ERRORI		(REG_INT_STAT_EWLI | REG_INT_STAT_FCSI |\
> +					 REG_INT_STAT_ALI | REG_INT_STAT_BEI)
> +
> +/* Bitfields of Interrupt Enable register */
> +#define REG_INT_ENA_SET			GENMASK(12, 0)
> +#define REG_INT_ENA_CLR			GENMASK(28, 16)
> +
> +/* Bitfields of Interrupt Mask register */
> +#define REG_INT_MASK_SET		GENMASK(12, 0)
> +#define REG_INT_MASK_CLR		GENMASK(28, 16)
> +
> +/* Bitfields of Normal Rate Configuration register */
> +#define REG_BTR_PROP			GENMASK(6, 0)
> +#define REG_BTR_PH1			GENMASK(12, 7)
> +#define REG_BTR_PH2			GENMASK(18, 13)
> +#define REG_BTR_BRP			GENMASK(22, 19)
> +#define REG_BTR_SJW			GENMASK(31, 27)
> +
> +/* Bitfields of FD Data Rate Configuration register */
> +#define REG_BTR_FD_PROP			GENMASK(6, 0)
> +#define REG_BTR_FD_PH1			GENMASK(12, 7)
> +#define REG_BTR_FD_PH2			GENMASK(18, 13)
> +#define REG_BTR_FD_BRP			GENMASK(26, 19)
> +#define REG_BTR_FD_SJW			GENMASK(31, 27)
> +
> +/* Bitfields of Error Threshold Configuration register */
> +#define REG_ERL_ERP			GENMASK(7, 0)
> +#define REG_ERL_EW			GENMASK(23, 16)
> +
> +/* Bitfields of Error Status register */
> +#define REG_FSTAT_ERA			BIT(0)
> +#define REG_FSTAT_ERP			BIT(1)
> +#define REG_FSTAT_BOF			BIT(2)
> +
> +#define REG_FSTAT_MASK			GENMASK(2, 0)
> +
> +/* Bitfields of Error Count register */
> +#define REG_ERC_TEC			GENMASK(8, 0)
> +#define REG_ERC_REC			GENMASK(24, 16)
> +
> +/* Bitfields of Rate Error Count register */
> +#define REG_BRE_NORM			GENMASK(15, 0)
> +#define REG_BRE_FD_DATA			GENMASK(31, 16)
> +
> +/* Bitfields of Error Count Debug register */
> +#define REG_CTR_PRES_CTPV		GENMASK(8, 0)
> +#define REG_CTR_PRES_PTX		BIT(9)
> +#define REG_CTR_PRES_PRX		BIT(10)
> +
> +/* Bitfields of Error Capture Status register */
> +#define REG_ERR_CAPT_POS		GENMASK(4, 0)
> +#define REG_ERR_CAPT_TYPE		GENMASK(7, 5)
> +
> +/* Bitfields of Retransmission Count register */
> +#define REG_RETX_CNT_VAL		GENMASK(3, 0)
> +
> +/* Bitfields of Lost Arbitration Capture register */
> +#define REG_ALC_BIT_POS			GENMASK(4, 0)
> +#define REG_ALC_ID_FIELD		GENMASK(7, 5)
> +
> +/* Bitfields of Transmission Delay Measurement register */
> +#define REG_TRV_DLY_VAL			GENMASK(6, 0)
> +
> +/* Bitfields of Second Sampling Point Configuration register */
> +#define REG_SSP_CFG_OFF			GENMASK(7, 0)
> +#define REG_SSP_CFG_SRC			GENMASK(9, 8)
> +#define REG_SSP_CFG_SAT			BIT(10)
> +
> +/* Bitfields of Receive Message Count register */
> +#define REG_RX_FR_CNT_VAL		GENMASK(31, 0)
> +
> +/* Bitfields of Transmit Message Count register */
> +#define REG_TX_FR_CNT_VAL		GENMASK(31, 0)
> +
> +/* Bitfields of Debug register */
> +#define REG_DEBUG_STF_CNT		GENMASK(2, 0)
> +#define REG_DEBUG_DSTF_CNT		GENMASK(5, 3)
> +#define REG_DEBUG_PC_ARB		BIT(6)
> +#define REG_DEBUG_PC_CON		BIT(7)
> +#define REG_DEBUG_PC_DAT		BIT(8)
> +#define REG_DEBUG_PC_STC		BIT(9)
> +#define REG_DEBUG_PC_CRC		BIT(10)
> +#define REG_DEBUG_PC_CRCD		BIT(11)
> +#define REG_DEBUG_PC_ACK		BIT(12)
> +#define REG_DEBUG_PC_ACKD		BIT(13)
> +#define REG_DEBUG_PC_EOF		BIT(14)
> +#define REG_DEBUG_PC_INT		BIT(15)
> +#define REG_DEBUG_PC_SUSP		BIT(16)
> +#define REG_DEBUG_PC_OVR		BIT(17)
> +#define REG_DEBUG_PC_SOF		BIT(18)
> +
> +/* Bitfields of Timestamp register */
> +#define REG_TS_TIMESTAMP		GENMASK(15, 0)
> +#define REG_TS_PSC			GENMASK(24, 16)
> +
> +/* Bitfields of Fractional Divider Ratio register */
> +#define REG_FRC_FRC_DBT			GENMASK(15, 8)
> +#define REG_FRC_FRC_NBT			GENMASK(7, 0)
> +
> +/* Bitfields of Filter A Mask register */
> +#define REG_FIL_A_MASK			GENMASK(28, 0)
> +
> +/* Bitfields of Filter A value register */
> +#define REG_FIL_A_VAL			GENMASK(28, 0)
> +
> +/* Bitfields of Filter B Mask register */
> +#define REG_FIL_B_MASK			GENMASK(28, 0)
> +
> +/* Bitfields of Filter B value register */
> +#define REG_FIL_B_VAL			GENMASK(28, 0)
> +
> +/* Bitfields of Filter C Mask register */
> +#define REG_FIL_C_MASK			GENMASK(28, 0)
> +
> +/* Bitfields of Filter C value register */
> +#define REG_FIL_C_VAL			GENMASK(28, 0)
> +
> +/* Bitfields of Range Filter Low Threshold register */
> +#define REG_FIL_R_LOW_VAL		GENMASK(28, 0)
> +
> +/* Bitfields of Range Filter High Threshold register */
> +#define REG_FIL_R_HI_VAL		GENMASK(28, 0)
> +
> +/* Bitfields of Filter Control register */
> +#define REG_FIL_CTRL_FANB		BIT(0)
> +#define REG_FIL_CTRL_FANE		BIT(1)
> +#define REG_FIL_CTRL_FAFB		BIT(2)
> +#define REG_FIL_CTRL_FAFE		BIT(3)
> +#define REG_FIL_CTRL_FBNB		BIT(4)
> +#define REG_FIL_CTRL_FBNE		BIT(5)
> +#define REG_FIL_CTRL_FBFB		BIT(6)
> +#define REG_FIL_CTRL_FBFE		BIT(7)
> +#define REG_FIL_CTRL_FCNB		BIT(8)
> +#define REG_FIL_CTRL_FCNE		BIT(9)
> +#define REG_FIL_CTRL_FCFB		BIT(10)
> +#define REG_FIL_CTRL_FCFE		BIT(11)
> +#define REG_FIL_CTRL_FRNB		BIT(12)
> +#define REG_FIL_CTRL_FRNE		BIT(13)
> +#define REG_FIL_CTRL_FRFB		BIT(14)
> +#define REG_FIL_CTRL_FRFE		BIT(15)
> +#define REG_FIL_CTRL_SFA		BIT(16)
> +#define REG_FIL_CTRL_SFB		BIT(17)
> +#define REG_FIL_CTRL_SFC		BIT(18)
> +#define REG_FIL_CTRL_SFR		BIT(19)
> +
> +/* Bitfields of Receive Buffer Information register */
> +#define REG_RX_MEM_INFO_BUFF_SIZE	GENMASK(12, 0)
> +#define REG_RX_MEM_INFO_MEM_FREE	GENMASK(28, 16)
> +
> +/* Bitfields of Receive Buffer Pointer register */
> +#define REG_RX_PTR_WPP			GENMASK(11, 0)
> +#define REG_RX_PTR_RPP			GENMASK(27, 16)
> +
> +/* Bitfields of Receive Buffer Status register */
> +#define REG_RX_STAT_RXE			BIT(0)
> +#define REG_RX_STAT_RXF			BIT(1)
> +#define REG_RX_STAT_RXMOF		BIT(2)
> +#define REG_RX_STAT_RXFRC		GENMASK(14, 4)
> +#define REG_RX_STAT_RTSOP		BIT(16)
> +
> +/* Bitfields of Receive Data register */
> +#define REG_RX_DATA_VAL			GENMASK(31, 0)
> +
> +/* Bitfields of Transmit Buffer Status register */
> +#define REG_TX_STAT_BRP			GENMASK(7, 0)
> +#define REG_TX_STAT_TXS			GENMASK(10, 8)
> +#define REG_TX_STAT_BS			GENMASK(31, 16)
> +
> +#define REG_TX_STAT_BS_TX_MASK_BASE	GENMASK(17, 16)
> +
> +/* Bitfields of Transmit Command register */
> +#define REG_TX_CMD_BAR			GENMASK(7, 0)
> +#define REG_TX_CMD_BCR			GENMASK(15, 8)
> +#define REG_TX_CMD_BSC			GENMASK(23, 16)
> +
> +/* Bitfields of Transmit Buffer Selection register */
> +#define REG_TX_SEL_BUF_SEL		GENMASK(3, 0)
> +#define REG_TX_SEL_BUF_CNT		GENMASK(7, 4)
> +
> +/* Loongson CANFD Frame format */
> +#define LOONGSON_CANFD_FRAME_META0	0x0
> +#define LOONGSON_CANFD_FRAME_META1	0x4
> +#define LOONGSON_CANFD_FRAME_DB_1	0x8
> +
> +/* Bitfields of FRAME META0 */
> +#define REG_FRAME_META0_ID_EXT		GENMASK(17, 0)
> +#define REG_FRAME_META0_ID_BASE		GENMASK(28, 18)
> +#define REG_FRAME_META0_RTR		BIT(29)	/* Remote Transmission Request, only in CAN2.0 */
> +/* Extended Identifier Type. 1: 29 bits ID, 0: 11 bits ID. */
> +#define REG_FRAME_META0_XDT		BIT(30)
> +#define REG_FRAME_META0_ESI		BIT(31)	/* Error State Indicator, only in CAN-FD */
> +
> +/* Bitfields of FRAME META1 */
> +#define REG_FRAME_META1_TIMESTAMP	GENMASK(15, 0)
> +#define REG_FRAME_META1_DLC		GENMASK(19, 16)	/* Data Length Code */
> +#define REG_FRAME_META1_BRS		BIT(20)	/* Bit Rate Shift, Only in CANFD */
> +/* Flexible Data-rate Format. 1: CANFD, 0: CAN2.0 */
> +#define REG_FRAME_META1_FDF		BIT(21)
> +#define REG_FRAME_META1_RWCNT		GENMASK(28, 24)	/* Read Word Counter */
> +
> +/* Bitfields of FRAME TEST */
> +#define REG_FRAME_TEST_FSTC		BIT(0)
> +#define REG_FRAME_TEST_FCRC		BIT(1)
> +#define REG_FRAME_TEST_SDLC		BIT(2)
> +#define REG_FRAME_TEST_TPRM		GENMASK(12, 8)
> +
> +#define DEV_NAME			"loongson_canfd"
> +#define LOONGSON_CANFD_ID		0xBABE
> +#define LOONGSON_CANFD_DW_BYTE		4
> +#define LOONGSON_CANFD_TXBUF_NUM	8
> +#define LOONGSON_CANFD_MAX_RTXTH	0xf
> +
> +/* Upper bound on the number of ISR drain iterations */
> +#define LOONGSON_CANFD_IRQ_LOOP_MAX		10000
> +
> +/*
> + * 16-bit counter couples resolution and window length:
> + *     window = 2^16 * tick
> + *
> + * A 2 us tick yields ~131 ms, well above the worst case a frame can
> + * stay in the 32 KB RX ring before NAPI dequeues it.
> + */
> +#define LOONGSON_CANFD_HWTS_TICK_NS		2000
> +#define LOONGSON_CANFD_HWTS_COUNTER_BITS	16
> +#define LOONGSON_CANFD_HWTS_SLEW_SHIFT		3
> +#define LOONGSON_CANFD_HWTS_SLEW_BAND_NS	2000
> +
> +/**
> + * struct loongson_canfd_hwts - Hardware timestamp state
> + * @cc: Cycle counter abstracting the 16-bit timestamp counter
> + * @tc: Nanosecond time counter built on @cc, anchored to CLOCK_REALTIME
> + * @lock: Serializes access to @tc (NAPI, TX interrupt, chip start)
> + * @window_ns: Length of the unambiguous window, 2^16 tick periods
> + * @tick_ns: Achieved tick period in nanoseconds
> + * @psc: Prescaler value currently programmed into REG_TS_PSC
> + * @ready: Timestamping is configured and usable
> + */
> +struct loongson_canfd_hwts {
> +	struct cyclecounter	cc;
> +	struct timecounter	tc;
> +	spinlock_t		lock;	/* protect the tc context */
> +	u64			window_ns;
> +	u32			tick_ns;
> +	u32			psc;
> +	bool			ready;
> +};
> +
> +/**
> + * struct loongson_canfd_rx_desc - Parsed on-wire frame descriptor
> + * @hdr0: First header word: ID / RTR / XDT / ESI
> + * @hdr1: Second header word: timestamp / DLC / BRS / FDF / RWCNT
> + * @rwcnt: Total 32-bit words the frame occupies in the ring
> + * @dbcnt: Payload size in bytes, (rwcnt - 2) * 4
> + * @is_can_fd: True if the FDF bit is set
> + * @is_rtr: True if the RTR bit is set (Classic CAN only)
> + */
> +struct loongson_canfd_rx_desc {
> +	u32	hdr0;
> +	u32	hdr1;
> +	u32	rwcnt;
> +	u32	dbcnt;
> +	bool	is_can_fd;
> +	bool	is_rtr;
> +};
> +
> +/**
> + * struct loongson_canfd_priv - Driver private data
> + * @can: CAN private data structure (must be first member)
> + * @napi: NAPI structure
> + * @regmap: Regmap of the CAN device
> + * @mem_res: Pointer to the CAN device resource (MMIO region)
> + * @tx_lock: Lock for synchronizing TX interrupt handling
> + * @stats_lock: Lock for protecting netdev statistics
> + * @hwts: Hardware timestamp state
> + */
> +struct loongson_canfd_priv {
> +	struct can_priv		can;
> +	struct napi_struct	napi;
> +	struct regmap		*regmap;
> +	struct resource		*mem_res;
> +	spinlock_t		tx_lock;	/* protect the sending queue */
> +	spinlock_t		stats_lock;	/* protect the ndev->stats */
> +	struct loongson_canfd_hwts	hwts;
> +};
> +
> +/**
> + * enum loongson_canfd_txbuf_sts - Status of TX buffer
> + * @TX_BS_IDLE: No transmission or processed.
> + * @TX_BS_VALID: Transmission successful.
> + * @TX_BS_FAIL: Transmission failed (retry limit exceeded).
> + * @TX_BS_CANCEL: Transmission cancelled.
> + */
> +enum loongson_canfd_txbuf_sts {
> +	TX_BS_IDLE	= 0x0,
> +	TX_BS_VALID	= 0x1,
> +	TX_BS_FAIL	= 0x2,
> +	TX_BS_CANCEL	= 0x3
> +};
> +
> +/**
> + * enum loongson_canfd_txbuf_cmd - Commands for TX buffer
> + * @TXT_CMD_ADD: Add buffer request (start transmission)
> + * @TXT_CMD_CANCEL: Cancel buffer request
> + * @TXT_CMD_SR_CLEAN: Clear send completion record
> + */
> +enum loongson_canfd_txbuf_cmd {
> +	TXT_CMD_ADD		= 0x01,
> +	TXT_CMD_CANCEL		= 0x02,
> +	TXT_CMD_SR_CLEAN	= 0x03
> +};
> +
> +static const struct can_bittiming_const loongson_canfd_bit_timing = {
> +	.name		= DEV_NAME,
> +	.tseg1_min	= 2,
> +	.tseg1_max	= 190,
> +	.tseg2_min	= 2,
> +	.tseg2_max	= 63,
> +	.sjw_max	= 31,
> +	.brp_min	= 1,
> +	.brp_max	= 15,
> +	.brp_inc	= 1,
> +};
> +
> +static const struct can_bittiming_const loongson_canfd_bit_timing_data = {
> +	.name		= DEV_NAME,
> +	.tseg1_min	= 2,
> +	.tseg1_max	= 190,
> +	.tseg2_min	= 2,
> +	.tseg2_max	= 63,
> +	.sjw_max	= 31,
> +	.brp_min	= 1,
> +	.brp_max	= 255,
> +	.brp_inc	= 1,
> +};
> +
> +static const struct can_tdc_const loongson_canfd_tdc_data = {
> +	.tdcv_min	= 0,
> +	.tdcv_max	= 0,
> +	.tdco_min	= 0,
> +	.tdco_max	= 255,
> +	.tdcf_min	= 0,
> +	.tdcf_max	= 0,
> +};
> +
> +/* Return @field when @cond is true, 0 otherwise. */
> +#define LOONGSON_CANFD_FIELD_IF(cond, field) (-(!!(cond)) & (field))
> +
> +/**
> + * loongson_canfd_enabled() - Check if controller is enabled
> + * @priv: Driver private data
> + *
> + * Return: true if enabled, false otherwise.
> + */
> +static bool loongson_canfd_enabled(struct loongson_canfd_priv *priv)
> +{
> +	return !!regmap_test_bits(priv->regmap, LOONGSON_CANFD_CONF, REG_CONF_ENA);
> +}
> +
> +/**
> + * loongson_canfd_disable_interrupts() - Disable all interrupts
> + * @priv: Driver private data
> + */
> +static void loongson_canfd_disable_interrupts(struct loongson_canfd_priv *priv)
> +{
> +	/* Clear all ENA bits, then set all MASK bits */
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, REG_INT_ENA_CLR);
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, REG_INT_MASK_SET);
> +}
> +
> +/**
> + * loongson_canfd_read_rx_word_fifo() - Read RX data in poll mode
> + * @priv: Driver private data
> + *
> + * Return: The next 32-bit data word from the RX data register.
> + */
> +static u32 loongson_canfd_read_rx_word_fifo(struct loongson_canfd_priv *priv)
> +{
> +	u32 data;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_DATA, &data);
> +	return data;
> +}
> +
> +/**
> + * loongson_canfd_rx_is_empty_fifo() - Poll mode: hardware FIFO empty check
> + * @priv: Driver private data
> + *
> + * Uses both RXE and RXFRC so a transient RXE during FIFO refill is not
> + * mistaken for an empty FIFO.
> + *
> + * Return: true if no frame is pending and RXE is set.
> + */
> +static bool loongson_canfd_rx_is_empty_fifo(struct loongson_canfd_priv *priv)
> +{
> +	u32 rx_stat, framecnt;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
> +	framecnt = FIELD_GET(REG_RX_STAT_RXFRC, rx_stat);
> +
> +	if (framecnt)
> +		return false;
> +
> +	/* RXE may transiently assert while the FIFO is being refilled */
> +	return !!(rx_stat & REG_RX_STAT_RXE);
> +}
> +
> +/**
> + * loongson_canfd_hwts_read_cycles() - Read the free-running timestamp counter
> + * @cc: Cycle counter whose read callback is being invoked
> + *
> + * Reads LOONGSON_CANFD_TS and extracts the 16-bit counter field (bits 15:0).
> + * The register is marked volatile in loongson_canfd_volatile_reg(), so
> + * regmap_read() bypasses the cache and returns a live value.
> + *
> + * Return: Current value of the 16-bit timestamp counter.
> + */
> +static u64 loongson_canfd_hwts_read_cycles(struct cyclecounter *cc)
> +{
> +	struct loongson_canfd_hwts *hwts =
> +		container_of(cc, struct loongson_canfd_hwts, cc);
> +	struct loongson_canfd_priv *priv =
> +		container_of(hwts, struct loongson_canfd_priv, hwts);
> +	u32 val;
> +
> +	/* REG_TS is volatile, so this returns a live counter value */
> +	regmap_read(priv->regmap, LOONGSON_CANFD_TS, &val);
> +
> +	return FIELD_GET(REG_TS_TIMESTAMP, val);
> +}
> +
> +/**
> + * loongson_canfd_hwts_anchor() - Refresh the nanosecond epoch
> + * @priv: Driver private data
> + *
> + * Calls timecounter_read() so a single wrap can be followed, then
> + * nudges the epoch toward CLOCK_REALTIME. A deviation of half a window
> + * or more means the epoch is worthless and is re-initialized instead.
> + *
> + * Caller must hold @hwts.lock.
> + */
> +static void loongson_canfd_hwts_anchor(struct loongson_canfd_priv *priv)
> +{
> +	struct loongson_canfd_hwts *hwts = &priv->hwts;
> +	s64 dev;
> +
> +	timecounter_read(&hwts->tc);
> +
> +	if (!hwts->window_ns)
> +		return;
> +
> +	dev = ktime_get_real_ns() - (s64)hwts->tc.nsec;
> +
> +	/* Whole windows lost: re-lock, do not slew (stamps must stay monotonic) */
> +	if (dev >= (s64)hwts->window_ns / 2 ||
> +	    dev <= -(s64)hwts->window_ns / 2) {
> +		timecounter_init(&hwts->tc, &hwts->cc, ktime_get_real_ns());
> +		return;
> +	}
> +
> +	if (dev > -LOONGSON_CANFD_HWTS_SLEW_BAND_NS &&
> +	    dev < LOONGSON_CANFD_HWTS_SLEW_BAND_NS)
> +		return;
> +
> +	timecounter_adjtime(&hwts->tc, dev >> LOONGSON_CANFD_HWTS_SLEW_SHIFT);
> +}
> +
> +/**
> + * loongson_canfd_hwts_ts() - Convert a raw 16-bit stamp to a ktime_t
> + * @priv: Driver private data
> + * @raw: Raw 16-bit timestamp taken from the frame
> + *
> + * Frames are always stamped before the driver reads them, so the
> + * result is folded into the most recent past window, making the
> + * reconstruction exact for any frame age below one full window.
> + *
> + * Return: ktime_t of @raw, in CLOCK_REALTIME nanoseconds.
> + */
> +static ktime_t loongson_canfd_hwts_ts(struct loongson_canfd_priv *priv, u32 raw)
> +{
> +	struct loongson_canfd_hwts *hwts = &priv->hwts;
> +	s64 age, ts;
> +
> +	guard(spinlock_irqsave)(&hwts->lock);
> +
> +	loongson_canfd_hwts_anchor(priv);
> +
> +	ts = (s64)timecounter_cyc2time(&hwts->tc, raw);
> +
> +	age = (s64)hwts->tc.nsec - ts;
> +	if (age < 0)
> +		age += (s64)hwts->window_ns;
> +
> +	return ns_to_ktime((s64)hwts->tc.nsec - age);
> +}
> +
> +/**
> + * loongson_canfd_hwts_now() - Timestamp of the current moment
> + * @priv: Driver private data
> + *
> + * Samples the counter before converting, so the result is never later
> + * than the moment of the call.
> + *
> + * Return: ktime_t of now, in CLOCK_REALTIME nanoseconds.
> + */
> +static ktime_t loongson_canfd_hwts_now(struct loongson_canfd_priv *priv)
> +{
> +	struct loongson_canfd_hwts *hwts = &priv->hwts;
> +
> +	/* Sample first: the counter is the earlier of the two instants */
> +	return loongson_canfd_hwts_ts(priv, (u32)hwts->cc.read(&hwts->cc));
> +}
> +
> +/**
> + * loongson_canfd_hwts_setup() - Program the prescaler and start the epoch
> + * @ndev: Network device
> + *
> + * The 16-bit free-running counter is extended to 64-bit nanoseconds
> + * via a cyclecounter/timecounter pair anchored to CLOCK_REALTIME. On
> + * failure @hwts.ready is left false; the controller can still run,
> + * just without timestamping.
> + *
> + * Return: 0 on success, negative error code on failure.
> + */
> +static int loongson_canfd_hwts_setup(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct loongson_canfd_hwts *hwts = &priv->hwts;
> +	u32 tick_hz, clk = priv->can.clock.freq;
> +	u64 frac = 0, div;
> +	int ret;
> +
> +	/* Disable timestamping until the new setup succeeds */
> +	WRITE_ONCE(hwts->ready, false);
> +
> +	if (!clk) {
> +		netdev_warn(ndev, "Zero clock, HW timestamp unavailable\n");
> +		return -EINVAL;
> +	}
> +
> +	/* tick = (psc + 1) / clk, aim for LOONGSON_CANFD_HWTS_TICK_NS */
> +	div = DIV_ROUND_CLOSEST_ULL((u64)LOONGSON_CANFD_HWTS_TICK_NS * clk,
> +				    NSEC_PER_SEC);
> +	div = clamp_t(u64, div, 1, FIELD_MAX(REG_TS_PSC) + 1ULL);
> +
> +	hwts->psc = div - 1;
> +	hwts->tick_ns = DIV_ROUND_CLOSEST_ULL(div * NSEC_PER_SEC, clk);
> +
> +	tick_hz = clk / (u32)div;
> +	if (!tick_hz) {
> +		netdev_warn(ndev, "Clock too slow for HW timestamp (%u Hz)\n",
> +			    clk);
> +		return -EINVAL;
> +	}
> +
> +	hwts->cc.read = loongson_canfd_hwts_read_cycles;
> +	hwts->cc.mask = CYCLECOUNTER_MASK(LOONGSON_CANFD_HWTS_COUNTER_BITS);
> +	clocks_calc_mult_shift(&hwts->cc.mult, &hwts->cc.shift,
> +			       tick_hz, NSEC_PER_SEC, 60);
> +
> +	hwts->window_ns = cyclecounter_cyc2ns(&hwts->cc,
> +					      hwts->cc.mask + 1, 0, &frac);
> +
> +	/* Update only the prescaler bits, leave the counter alone */
> +	ret = regmap_update_bits(priv->regmap, LOONGSON_CANFD_TS, REG_TS_PSC,
> +				 FIELD_PREP(REG_TS_PSC, hwts->psc));
> +	if (ret) {
> +		netdev_err(ndev, "Failed to program TS prescaler: %d\n", ret);
> +		return ret;
> +	}
> +
> +	scoped_guard(spinlock_irqsave, &hwts->lock) {
> +		timecounter_init(&hwts->tc, &hwts->cc, ktime_get_real_ns());
> +	}
> +
> +	WRITE_ONCE(hwts->ready, true);
> +
> +	netdev_dbg(ndev, "HW timestamp: psc=%u, tick=%u ns, window=%llu ns\n",
> +		   hwts->psc, hwts->tick_ns, hwts->window_ns);
> +
> +	return 0;
> +}
> +
> +/**
> + * loongson_canfd_reset() - Issue soft reset to controller
> + * @ndev: Network device
> + *
> + * The reset bit does not self-clear on this silicon; the mode write
> + * below re-programs the register, so a timeout is only logged.
> + */
> +static void loongson_canfd_reset(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	int ret, val;
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_MODE, REG_MODE_RST);
> +
> +	ret = regmap_read_poll_timeout(priv->regmap, LOONGSON_CANFD_MODE,
> +				       val, !(val & REG_MODE_RST),
> +				       10, 10 * USEC_PER_MSEC);
> +	if (ret)
> +		dev_dbg(ndev->dev.parent,
> +			"Soft reset did not self-clear: %d\n", ret);
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_MODE, REG_MODE_RXBAM | REG_MODE_BUFM);
> +}
> +
> +/**
> + * loongson_canfd_set_btr() - Set CAN bit timing registers
> + * @ndev: Network device
> + * @bt: Bit timing structure
> + * @nominal: true for nominal timing, false for data timing
> + */
> +static void loongson_canfd_set_btr(struct net_device *ndev,
> +				   struct can_bittiming *bt, bool nominal)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 prop = bt->prop_seg, phase1 = bt->phase_seg1;
> +	u32 ph1_max = FIELD_MAX(REG_BTR_PH1);
> +	u32 btr = 0;
> +
> +	/* PROP is 7 bits but PH1 only 6; move the excess into PROP */
> +	if (phase1 > ph1_max) {
> +		prop += phase1 - ph1_max;
> +		phase1 = ph1_max;
> +	}
> +
> +	if (nominal) {
> +		btr = FIELD_PREP(REG_BTR_PROP, prop) |
> +		      FIELD_PREP(REG_BTR_PH1, phase1) |
> +		      FIELD_PREP(REG_BTR_PH2, bt->phase_seg2) |
> +		      FIELD_PREP(REG_BTR_BRP, bt->brp) |
> +		      FIELD_PREP(REG_BTR_SJW, bt->sjw);
> +
> +		regmap_write(priv->regmap, LOONGSON_CANFD_BTR_NORM, btr);
> +	} else {
> +		btr = FIELD_PREP(REG_BTR_FD_PROP, prop) |
> +		      FIELD_PREP(REG_BTR_FD_PH1, phase1) |
> +		      FIELD_PREP(REG_BTR_FD_PH2, bt->phase_seg2) |
> +		      FIELD_PREP(REG_BTR_FD_BRP, bt->brp) |
> +		      FIELD_PREP(REG_BTR_FD_SJW, bt->sjw);
> +
> +		regmap_write(priv->regmap, LOONGSON_CANFD_BTR_FD, btr);
> +	}
> +}
> +
> +/**
> + * loongson_canfd_set_bittiming() - Set nominal bit timing
> + * @ndev: Network device
> + *
> + * Return: 0 on success, negative on error.
> + */
> +static int loongson_canfd_set_bittiming(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct can_bittiming *bt = &priv->can.bittiming;
> +
> +	loongson_canfd_set_btr(ndev, bt, true);
> +
> +	return 0;
> +}
> +
> +/**
> + * loongson_canfd_set_secondary_sample_point() - Configure secondary sample point
> + * @ndev: Network device
> + *
> + * Return: 0 on success, -EBUSY if the controller is enabled.
> + */
> +static int loongson_canfd_set_secondary_sample_point(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 ctrlmode = priv->can.ctrlmode;
> +	u32 ssp_cfg;
> +
> +	if (loongson_canfd_enabled(priv)) {
> +		netdev_err(ndev, "Cannot set SSP while CAN is enabled\n");
> +		return -EBUSY;
> +	}
> +
> +	if (ctrlmode & CAN_CTRLMODE_TDC_MANUAL)
> +		/* Manual TDC: 2'd2, delay = SSP_OFF */
> +		ssp_cfg = FIELD_PREP(REG_SSP_CFG_SRC, 2) |
> +			  FIELD_PREP(REG_SSP_CFG_OFF, priv->can.fd.tdc.tdco);
> +	else if (ctrlmode & CAN_CTRLMODE_TDC_AUTO)
> +		/* Auto TDC: 2'd0, delay = TRV_DLY + SSP_OFF */
> +		ssp_cfg = FIELD_PREP(REG_SSP_CFG_SRC, 0) |
> +			  FIELD_PREP(REG_SSP_CFG_OFF, priv->can.fd.tdc.tdco);
> +	else
> +		/* TDC Disabled: 2'd1, use standard sample point */
> +		ssp_cfg = FIELD_PREP(REG_SSP_CFG_SRC, 1) |
> +			  FIELD_PREP(REG_SSP_CFG_OFF, 0);
> +
> +	return regmap_write(priv->regmap, LOONGSON_CANFD_SSP_CFG, ssp_cfg);
> +}
> +
> +/**
> + * loongson_canfd_set_data_bittiming() - Set data bit timing and SSP
> + * @ndev: Network device
> + *
> + * BTR_FD and SSP_CFG are refreshed together because the SSP position
> + * depends on the data bit time.
> + *
> + * Return: 0 on success, negative on error.
> + */
> +static int loongson_canfd_set_data_bittiming(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct can_bittiming *dbt = &priv->can.fd.data_bittiming;
> +	int ret;
> +
> +	loongson_canfd_set_btr(ndev, dbt, false);
> +
> +	ret = loongson_canfd_set_secondary_sample_point(ndev);
> +	if (ret < 0)
> +		return ret;
> +
> +	return 0;
> +}
> +
> +/**
> + * loongson_canfd_get_auto_tdcv() - Get transmitter delay compensation value
> + * @ndev: Network device
> + * @tdcv: Pointer to store TDCV value
> + *
> + * Return: 0 on success, -EOPNOTSUPP on CAN_CTRLMODE_TDC_AUTO is not set.
> + */
> +static int loongson_canfd_get_auto_tdcv(const struct net_device *ndev, u32 *tdcv)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 val;
> +
> +	if (!(priv->can.ctrlmode & CAN_CTRLMODE_TDC_AUTO))
> +		return -EOPNOTSUPP;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_TRV_DLY, &val);
> +	*tdcv = FIELD_GET(REG_TRV_DLY_VAL, val);
> +
> +	/* Hardware measurement is complete only when TRV_DLY is not 0 */
> +	if (!*tdcv)
> +		return -EAGAIN;
> +
> +	return 0;
> +}
> +
> +/**
> + * loongson_canfd_apply_ctrlmode() - Apply CAN ctrlmode to MODE/CONF
> + * @priv: Driver private data
> + */
> +static void loongson_canfd_apply_ctrlmode(struct loongson_canfd_priv *priv)
> +{
> +	u32 ctrlmode = priv->can.ctrlmode;
> +	u32 mode, conf;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_MODE, &mode);
> +
> +	mode &= ~(REG_MODE_BMM | REG_MODE_FDE | REG_MODE_ACF | REG_MODE_TSTM);
> +	mode |= REG_MODE_ITSM | REG_MODE_RTSOP;
> +	mode |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_LISTENONLY,
> +					REG_MODE_BMM);
> +	mode |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_FD,
> +					REG_MODE_FDE);
> +	mode |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_PRESUME_ACK,
> +					REG_MODE_ACF);
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_MODE, mode);
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_CONF, &conf);
> +
> +	conf &= ~(REG_CONF_ILBP | REG_CONF_NISOFD |
> +		  REG_CONF_RTRLE | REG_CONF_RTRTH);
> +
> +	/* RTRLE always on; ONE_SHOT only decides RTRTH */
> +	conf |= REG_CONF_RTRLE;
> +	conf |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_LOOPBACK,
> +					REG_CONF_ILBP);
> +	conf |= LOONGSON_CANFD_FIELD_IF(ctrlmode & CAN_CTRLMODE_FD_NON_ISO,
> +					REG_CONF_NISOFD);
> +	conf |= LOONGSON_CANFD_FIELD_IF(!(ctrlmode & CAN_CTRLMODE_ONE_SHOT),
> +					FIELD_PREP(REG_CONF_RTRTH,
> +						   LOONGSON_CANFD_MAX_RTXTH));
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_CONF, conf);
> +}
> +
> +/**
> + * loongson_canfd_chip_start() - Start the CAN controller
> + * @ndev: Network device
> + *
> + * Configures bit timing, SSP, interrupt enables, and enables the controller.
> + *
> + * Return: 0 on success, negative error on failure.
> + */
> +static int loongson_canfd_chip_start(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 int_ena, int_msk;
> +	int ret;
> +
> +	ret = loongson_canfd_set_bittiming(ndev);
> +	if (ret < 0)
> +		return ret;
> +
> +	/* Programs BTR_FD and SSP_CFG together */
> +	ret = loongson_canfd_set_data_bittiming(ndev);
> +	if (ret < 0)
> +		return ret;
> +
> +	loongson_canfd_apply_ctrlmode(priv);
> +
> +	int_ena = REG_INT_STAT_RBNEI | REG_INT_STAT_TXBHCI |
> +		  REG_INT_STAT_EWLI  | REG_INT_STAT_FCSI   |
> +		  REG_INT_STAT_DOI;
> +
> +	if (priv->can.ctrlmode & CAN_CTRLMODE_BERR_REPORTING)
> +		int_ena |= REG_INT_STAT_ALI | REG_INT_STAT_BEI;
> +
> +	int_msk = REG_INT_MASK_SET & ~int_ena;
> +
> +	/* Clear stale mask/ena bits left by disable_interrupts() */
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, REG_INT_MASK_CLR);
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, REG_INT_ENA_CLR);
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, int_msk);
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_ENA, int_ena);
> +
> +	loongson_canfd_hwts_setup(ndev);
> +
> +	priv->can.state = CAN_STATE_ERROR_ACTIVE;
> +
> +	regmap_update_bits(priv->regmap, LOONGSON_CANFD_CONF, REG_CONF_ENA,
> +			   REG_CONF_ENA);
> +
> +	return 0;
> +}
> +
> +/**
> + * loongson_canfd_chip_stop() - Stop the controller
> + * @ndev: Network device
> + */
> +static void loongson_canfd_chip_stop(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	loongson_canfd_disable_interrupts(priv);
> +	regmap_update_bits(priv->regmap, LOONGSON_CANFD_CONF, REG_CONF_ENA, 0);
> +
> +	priv->can.state = CAN_STATE_STOPPED;
> +
> +	/* The timestamp counter only runs while the controller is enabled */
> +	WRITE_ONCE(priv->hwts.ready, false);
> +}
> +
> +/**
> + * loongson_canfd_do_set_mode() - Change operation mode
> + * @ndev: Network device
> + * @mode: Mode to set
> + *
> + * Return: 0 on success, negative error on failure.
> + */
> +static int loongson_canfd_do_set_mode(struct net_device *ndev, enum can_mode mode)
> +{
> +	int ret;
> +
> +	switch (mode) {
> +	case CAN_MODE_START:
> +		loongson_canfd_reset(ndev);
> +
> +		ret = loongson_canfd_chip_start(ndev);
> +		if (ret < 0) {
> +			netdev_err(ndev, "Failed to start chip\n");
> +			return ret;
> +		}
> +
> +		netif_wake_queue(ndev);
> +		break;
> +	default:
> +		ret = -EOPNOTSUPP;
> +		break;
> +	}
> +
> +	return ret;
> +}
> +
> +/**
> + * loongson_canfd_txbuf_freed() - Check if at least one TX buffer is free
> + * @priv: Driver private data
> + *
> + * Examines the TX buffer pending status (BRP bits). If any of the 8 buffers
> + * is not pending, it indicates a free buffer.
> + *
> + * Return: true if at least one TX buffer is free, false if all pending.
> + */
> +static bool loongson_canfd_txbuf_freed(struct loongson_canfd_priv *priv)
> +{
> +	return !regmap_test_bits(priv->regmap, LOONGSON_CANFD_TX_STAT, REG_TX_STAT_BRP);
> +}
> +
> +/**
> + * loongson_canfd_get_txbuf_sts() - Get status of a TX buffer
> + * @priv: Driver private data
> + * @buf_id: Buffer index (0-based, 0..7)
> + *
> + * Reads the 2-bit BS field for @buf_id from TX_STAT. The field is located
> + * at bits (16 + 2 * buf_id) .. (17 + 2 * buf_id).
> + *
> + * Return: Status of the specified TX buffer as an enum value.
> + */
> +static enum loongson_canfd_txbuf_sts
> +loongson_canfd_get_txbuf_sts(struct loongson_canfd_priv *priv, u8 buf_id)
> +{
> +	u32 sts, mask;
> +
> +	/* BS field for buf_id lives at bits (16 + 2*buf_id):(17 + 2*buf_id) */
> +	mask = REG_TX_STAT_BS_TX_MASK_BASE << (2 * buf_id);
> +	regmap_read(priv->regmap, LOONGSON_CANFD_TX_STAT, &sts);
> +
> +	return field_get(mask, sts);
> +}
> +
> +/**
> + * loongson_canfd_txbuf_is_writable() - Check if a TX buffer is writable
> + * @priv: Driver private data
> + * @buf_id: Buffer index (0-based)
> + *
> + * A buffer accepts a new frame only if:
> + *   - its BRP bit is clear (not pending), and
> + *   - its 2-bit BS field is TX_BS_IDLE (previous frame finished).
> + *
> + * Return: true if the buffer is in a writable state, false otherwise.
> + */
> +static bool loongson_canfd_txbuf_is_writable(struct loongson_canfd_priv *priv, u8 buf_id)
> +{
> +	enum loongson_canfd_txbuf_sts bs;
> +
> +	/* Writable only if BS is idle and BRP bit is clear */
> +	bs = loongson_canfd_get_txbuf_sts(priv, buf_id);
> +	if (bs)
> +		return false;
> +
> +	return !regmap_test_bits(priv->regmap, LOONGSON_CANFD_TX_STAT, BIT(buf_id));
> +}
> +
> +/**
> + * loongson_canfd_set_txbuf_cmd() - Apply command on TX buffer
> + * @ndev: Network device
> + * @cmd: Command to apply (TXT_CMD_ADD, TXT_CMD_CANCEL, TXT_CMD_SR_CLEAN)
> + * @buf_id: Buffer index (0-based)
> + *
> + * Writes the appropriate command bit (BAR, BCR, or BSC) for @buf_id into
> + * the TX_CMD register. Each command field is 8 bits wide, so the buffer
> + * index is encoded as BIT(buf_id).
> + *
> + * Return: 0 on success, negative error code on failure.
> + */
> +static int loongson_canfd_set_txbuf_cmd(struct net_device *ndev,
> +					enum loongson_canfd_txbuf_cmd cmd, u8 buf_id)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 mask;
> +
> +	switch (cmd) {
> +	case TXT_CMD_ADD:
> +		mask = REG_TX_CMD_BAR;
> +		break;
> +	case TXT_CMD_CANCEL:
> +		mask = REG_TX_CMD_BCR;
> +		break;
> +	case TXT_CMD_SR_CLEAN:
> +		mask = REG_TX_CMD_BSC;
> +		break;
> +	default:
> +		netdev_err(ndev, "Unknown command id: %x\n", cmd);
> +		return -EINVAL;
> +	}
> +
> +	/* Each command field is 8 bits wide: buffer index is BIT(buf_id) */
> +	return regmap_write(priv->regmap, LOONGSON_CANFD_TX_CMD,
> +			    field_prep(mask, BIT(buf_id)));
> +}
> +
> +/**
> + * loongson_canfd_insert_frame() - Insert a frame into TX buffer
> + * @priv: Driver private data
> + * @cf: CAN frame to transmit
> + * @buf: Buffer index (0-based)
> + * @isfdf: true for CAN FD frame, false for CAN 2.0
> + *
> + * Writes the frame header (META0, META1) and payload into the selected
> + * TX buffer. For RTR frames, no payload is written.
> + *
> + * Return: 0 on success, negative error code on failure.
> + */
> +static int loongson_canfd_insert_frame(struct loongson_canfd_priv *priv,
> +				       const struct canfd_frame *cf, u8 buf,
> +				       bool isfdf)
> +{
> +	u32 frame_hdr0, frame_hdr1 = 0;
> +	int ret;
> +
> +	if (buf >= LOONGSON_CANFD_TXBUF_NUM)
> +		return -EINVAL;
> +
> +	if (!loongson_canfd_txbuf_is_writable(priv, buf))
> +		return -EBUSY;
> +
> +	if (cf->can_id & CAN_EFF_FLAG) {
> +		frame_hdr0 = cf->can_id & CAN_EFF_MASK;
> +		frame_hdr0 |= REG_FRAME_META0_XDT;
> +	} else {
> +		frame_hdr0 = FIELD_PREP(REG_FRAME_META0_ID_BASE,
> +					cf->can_id & CAN_SFF_MASK);
> +	}
> +
> +	if (cf->can_id & CAN_RTR_FLAG)
> +		frame_hdr0 |= REG_FRAME_META0_RTR;
> +
> +	if (isfdf) {
> +		frame_hdr1 = REG_FRAME_META1_FDF;
> +		if (cf->flags & CANFD_BRS)
> +			frame_hdr1 |= REG_FRAME_META1_BRS;
> +		frame_hdr1 |= FIELD_PREP(REG_FRAME_META1_DLC,
> +					 can_fd_len2dlc(cf->len));
> +	} else {
> +		/*
> +		 * Classic CAN: can_get_cc_dlc() honours CC_LEN8_DLC so
> +		 * len8_dlc can request DLC 9..15 for len == 8.
> +		 */
> +		struct can_frame *ccf = (struct can_frame *)cf;
> +
> +		frame_hdr1 |= FIELD_PREP(REG_FRAME_META1_DLC,
> +					 can_get_cc_dlc(ccf, priv->can.ctrlmode));
> +	}
> +
> +	ret = regmap_write(priv->regmap, LOONGSON_CANFD_TX_SEL, buf);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(priv->regmap,
> +			   LOONGSON_CANFD_TX_DATA_1 + LOONGSON_CANFD_FRAME_META0,
> +			   frame_hdr0);
> +	if (ret)
> +		return ret;
> +
> +	ret = regmap_write(priv->regmap,
> +			   LOONGSON_CANFD_TX_DATA_1 + LOONGSON_CANFD_FRAME_META1,
> +			   frame_hdr1);
> +	if (ret)
> +		return ret;
> +
> +	/* RTR frames carry no payload */
> +	if (cf->can_id & CAN_RTR_FLAG)
> +		return 0;
> +
> +	for (unsigned int i = 0; i < cf->len; i += LOONGSON_CANFD_DW_BYTE) {
> +		unsigned int remain = cf->len - i;
> +		u32 word;
> +
> +		if (remain >= LOONGSON_CANFD_DW_BYTE) {
> +			word = get_unaligned_le32(cf->data + i);
> +		} else {
> +			u8 buf[LOONGSON_CANFD_DW_BYTE] = { 0 };
> +
> +			memcpy(buf, cf->data + i, remain);
> +			word = get_unaligned_le32(buf);
> +		}
> +
> +		ret = regmap_write(priv->regmap,
> +				   LOONGSON_CANFD_TX_DATA_1 +
> +				   LOONGSON_CANFD_FRAME_DB_1 + i,
> +				   word);
> +		if (ret)
> +			return ret;
> +	}
> +
> +	return 0;
> +}
> +
> +/**
> + * loongson_canfd_find_free_txbuf() - Find a TX buffer ready for immediate write
> + * @priv: Driver private data
> + *
> + * The hardware requires both conditions to be met for a buffer to accept
> + * a new frame:
> + *   - its BRP bit indicates the buffer is not pending (tx_brp bit clear)
> + *   - its 2-bit BS field indicates the previous frame finished (tx_bs bits clear)
> + *
> + * Return: buffer index 0..LOONGSON_CANFD_TXBUF_NUM-1, or -1 if none free.
> + */
> +static int loongson_canfd_find_free_txbuf(struct loongson_canfd_priv *priv)
> +{
> +	unsigned long tx_brp;
> +	u32 tx_stat, i;
> +	u16 tx_bs;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_TX_STAT, &tx_stat);
> +	tx_brp = FIELD_GET(REG_TX_STAT_BRP, tx_stat);
> +	tx_bs  = FIELD_GET(REG_TX_STAT_BS, tx_stat);
> +
> +	/* Need both BRP clear and BS idle; skip slots still echoing */
> +	for_each_clear_bit(i, &tx_brp, LOONGSON_CANFD_TXBUF_NUM) {
> +		u32 mask = 0x3 << (i * 2);
> +
> +		if (!(tx_bs & mask)) {
> +			if (priv->can.echo_skb[i])
> +				continue;
> +			return i;
> +		}
> +	}
> +
> +	return -1;
> +}
> +
> +/**
> + * loongson_canfd_hdr_plausible() - Decide whether a header pair is real
> + * @hdr0: META0 word
> + * @hdr1: META1 word
> + *
> + * Cross-checks META0/META1 field widths against protocol limits and
> + * validates RWCNT against the frame type. The RTR check must stay
> + * here: this is also used as a DMA resync probe and needs the full
> + * discriminator to avoid mistaking a payload word for a header.
> + *
> + * Return: true if the pair is consistent with a real frame header.
> + */
> +static bool loongson_canfd_hdr_plausible(u32 hdr0, u32 hdr1)
> +{
> +	u32 rwcnt  = FIELD_GET(REG_FRAME_META1_RWCNT, hdr1);
> +	u32 dlc    = FIELD_GET(REG_FRAME_META1_DLC,   hdr1);
> +	bool isfd  = !!(hdr1 & REG_FRAME_META1_FDF);
> +	bool isrtr = !!(hdr0 & REG_FRAME_META0_RTR);
> +	u32 payload_len, expected_rwcnt;
> +
> +	/* 2 header words + payload, within CAN FD limits */
> +	if (rwcnt < 2 || rwcnt > 2 + CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE)
> +		return false;
> +
> +	/* RTR is a Classic CAN concept */
> +	if (isfd && isrtr)
> +		return false;
> +
> +	/* Standard ID fits in 11 bits */
> +	if (!(hdr0 & REG_FRAME_META0_XDT) &&
> +	    FIELD_GET(REG_FRAME_META0_ID_BASE, hdr0) > CAN_SFF_MASK)

[Severity: Low]
Can this check ever reject anything?

REG_FRAME_META0_ID_BASE is GENMASK(28, 18), an 11-bit field. FIELD_GET()
on it returns at most 0x7ff, which equals CAN_SFF_MASK, so the
comparison is always false. Standard-ID headers are never filtered
here.

The kernel-doc describes loongson_canfd_hdr_plausible() as the full
discriminator that stops payload words from being taken as a header.
A dead test weakens that.

Was the intent to require REG_FRAME_META0_ID_EXT (bits 17:0) to be
zero when XDT is clear? If the controller does not guarantee that, the
check should probably be dropped instead.

The & CAN_SFF_MASK applied after the same FIELD_GET() in
loongson_canfd_rx_deliver() is also a no-op.

> +		return false;
> +
> +	/* RTR frames carry no payload */
> +	if (isrtr)
> +		return rwcnt == 2;
> +
> +	/*
> +	 * RWCNT and DLC are independent fields; a real data frame always
> +	 * satisfies rwcnt == 2 + DIV_ROUND_UP(payload_len, 4).
> +	 */
> +	if (isfd)
> +		payload_len = can_fd_dlc2len(dlc);
> +	else
> +		payload_len = min_t(u32, dlc, CAN_MAX_DLEN);
> +
> +	expected_rwcnt = 2 + DIV_ROUND_UP(payload_len, LOONGSON_CANFD_DW_BYTE);
> +	return rwcnt == expected_rwcnt;
> +}
> +
> +/**
> + * loongson_canfd_rx_note_bad() - Account a malformed frame and resync the FIFO
> + * @ndev: Network device
> + * @rwcnt: Raw RWCNT field from the rejected frame's META1 word
> + *
> + * @rwcnt may be a sentinel from rx_hdr_valid(), so the payload drain
> + * is clamped to the maximum legal CAN FD size.
> + */
> +static void loongson_canfd_rx_note_bad(struct net_device *ndev, u32 rwcnt)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 words, data;
> +
> +	ndev->stats.rx_dropped++;
> +
> +	words = rwcnt > 2 ?
> +		min_t(u32, rwcnt - 2,
> +		      CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE) : 0;
> +	while (words--)
> +		regmap_read(priv->regmap, LOONGSON_CANFD_RX_DATA, &data);
> +}
> +
> +/**
> + * loongson_canfd_rx_hdr_valid() - Validate a frame header pair
> + * @ndev: Network device
> + * @d: Output descriptor, filled with all parsed fields
> + *
> + * Reads and sanity-checks the two header words. The caller must drain
> + * the remaining payload words when false is returned.
> + *
> + * Return: true if the header is valid, false otherwise.
> + */
> +static bool loongson_canfd_rx_hdr_valid(struct net_device *ndev,
> +					struct loongson_canfd_rx_desc *d)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	d->hdr0 = loongson_canfd_read_rx_word_fifo(priv);
> +	d->hdr1 = loongson_canfd_read_rx_word_fifo(priv);
> +
> +	d->rwcnt = FIELD_GET(REG_FRAME_META1_RWCNT, d->hdr1);
> +	if (d->rwcnt < 2) {
> +		/* Hand the caller a worst-case drain length */
> +		d->rwcnt = 2 + CANFD_MAX_DLEN / LOONGSON_CANFD_DW_BYTE;
> +		return false;
> +	}
> +
> +	d->dbcnt = (d->rwcnt - 2) * LOONGSON_CANFD_DW_BYTE;
> +	if (d->dbcnt > CANFD_MAX_DLEN)
> +		return false;
> +
> +	d->is_can_fd = !!(d->hdr1 & REG_FRAME_META1_FDF);
> +	d->is_rtr    = !!(d->hdr0 & REG_FRAME_META0_RTR);
> +
> +	/* Classic CAN cannot exceed 8 bytes; oversized dbcnt would overflow */
> +	if (!d->is_can_fd && d->dbcnt > CAN_MAX_DLEN)
> +		return false;
> +
> +	/* Reject payload words that happen to sit at a header boundary */
> +	if (!loongson_canfd_hdr_plausible(d->hdr0, d->hdr1))
> +		return false;
> +
> +	return true;
> +}
> +
> +/**
> + * loongson_canfd_rx_deliver() - Build and deliver the skb for one frame
> + * @ndev: Network device
> + * @d: Validated frame descriptor
> + *
> + * Return: 1 on success, -ENOMEM if skb allocation failed.
> + */
> +static int loongson_canfd_rx_deliver(struct net_device *ndev,
> +				     const struct loongson_canfd_rx_desc *d)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	struct canfd_frame *cfd;
> +	struct can_frame *ccf;
> +	struct sk_buff *skb;
> +	u32 dlc, i;
> +
> +	if (d->is_can_fd) {
> +		skb = alloc_canfd_skb(ndev, &cfd);
> +	} else {
> +		skb = alloc_can_skb(ndev, &ccf);
> +		cfd = (struct canfd_frame *)ccf;
> +	}
> +
> +	if (unlikely(!skb))
> +		return -ENOMEM;
> +
> +	if (d->hdr0 & REG_FRAME_META0_XDT)
> +		cfd->can_id = (d->hdr0 & CAN_EFF_MASK) | CAN_EFF_FLAG;
> +	else
> +		cfd->can_id = FIELD_GET(REG_FRAME_META0_ID_BASE, d->hdr0) &
> +			      CAN_SFF_MASK;
> +
> +	cfd->flags = 0;
> +
> +	if (d->is_can_fd) {
> +		if (d->hdr1 & REG_FRAME_META1_BRS)
> +			cfd->flags |= CANFD_BRS;
> +		if (d->hdr0 & REG_FRAME_META0_ESI)
> +			cfd->flags |= CANFD_ESI;
> +	} else if (d->is_rtr) {
> +		cfd->can_id |= CAN_RTR_FLAG;
> +	}
> +
> +	dlc = FIELD_GET(REG_FRAME_META1_DLC, d->hdr1);
> +	if (d->is_can_fd)
> +		cfd->len = can_fd_dlc2len(dlc);
> +	else
> +		can_frame_set_cc_len(ccf, dlc, priv->can.ctrlmode);
> +
> +	/*
> +	 * dbcnt is authoritative. Clamp len to it so uninitialized skb
> +	 * bytes are never exposed; RTR frames carry no payload and are
> +	 * the exception.
> +	 */
> +	if (!d->is_rtr && unlikely(cfd->len > d->dbcnt))
> +		cfd->len = d->dbcnt;
> +
> +	for (i = 0; i < d->dbcnt; i += LOONGSON_CANFD_DW_BYTE)
> +		put_unaligned_le32(loongson_canfd_read_rx_word_fifo(priv),
> +				   cfd->data + i);
> +
> +	if (!d->is_rtr)
> +		stats->rx_bytes += cfd->len;
> +	stats->rx_packets++;
> +
> +	/* Raw 0 is a legal counter sample on the wrap boundary */
> +	if (READ_ONCE(priv->hwts.ready)) {
> +		u32 raw = FIELD_GET(REG_FRAME_META1_TIMESTAMP, d->hdr1);
> +
> +		skb_hwtstamps(skb)->hwtstamp = loongson_canfd_hwts_ts(priv, raw);
> +	}
> +
> +	netif_receive_skb(skb);
> +	return 1;
> +}
> +
> +/**
> + * loongson_canfd_rx_one_frame_fifo() - Process one RX step in FIFO mode
> + * @ndev: Network device
> + *
> + * Reads one frame header, validates it, and delivers the frame.
> + * Malformed frames are dropped and accounted internally.
> + *
> + * Return: 1 - one frame consumed (delivered or dropped on purpose)
> + */
> +static int loongson_canfd_rx_one_frame_fifo(struct net_device *ndev)
> +{
> +	struct loongson_canfd_rx_desc d;
> +	int ret;
> +
> +	if (!loongson_canfd_rx_hdr_valid(ndev, &d)) {
> +		loongson_canfd_rx_note_bad(ndev, d.rwcnt);
> +		return 1;
> +	}
> +
> +	ret = loongson_canfd_rx_deliver(ndev, &d);
> +	if (ret == -ENOMEM)
> +		loongson_canfd_rx_note_bad(ndev, d.rwcnt);
> +
> +	return 1;
> +}
> +
> +/**
> + * loongson_canfd_rx_overrun_event() - Account and clear a hardware RX overrun
> + * @ndev: Network device
> + *
> + * Called from hard IRQ context on INT_STAT.DOI, so the error frame is
> + * queued via netif_rx() and logging is rate-limited. DOR is cleared
> + * with REG_CMD_CDO; if the FIFO is still full it re-latches and DOI
> + * fires again.
> + *
> + * Return: true if DOR was latched and has been cleared, false if
> + *         this was a stale DOI (no live DOR).
> + */
> +static bool loongson_canfd_rx_overrun_event(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	struct can_frame *cf;
> +	struct sk_buff *skb;
> +	u32 stat;
> +
> +	/* DOR is the authoritative sticky flag, DOI is only a hint */
> +	regmap_read(priv->regmap, LOONGSON_CANFD_STAT, &stat);
> +	if (!(stat & REG_STAT_DOR))
> +		return false;
> +
> +	scoped_guard(spinlock_irqsave, &priv->stats_lock) {
> +		stats->rx_over_errors++;
> +		stats->rx_errors++;
> +	}
> +
> +	if (net_ratelimit())
> +		netdev_err(ndev, "Rx Overflow\n");
> +
> +	/*
> +	 * Overrun is a controller condition, not a bus error: report it
> +	 * as CAN_ERR_CRTL/CAN_ERR_CRTL_RX_OVERFLOW. A failed allocation
> +	 * only loses the notification.
> +	 */
> +	skb = alloc_can_err_skb(ndev, &cf);
> +	if (skb) {
> +		cf->can_id |= CAN_ERR_CRTL;
> +		cf->data[1] = CAN_ERR_CRTL_RX_OVERFLOW;
> +
> +		scoped_guard(spinlock_irqsave, &priv->stats_lock) {
> +			stats->rx_packets++;
> +			stats->rx_bytes += cf->len;
> +		}
> +
> +		/* Hard IRQ: use netif_rx(), not netif_receive_skb() */
> +		netif_rx(skb);
> +	}
> +
> +	/*
> +	 * Clear the sticky DOR first. The caller clears DOI right after
> +	 * this returns; if DOR were still latched at that point, the
> +	 * hardware would immediately re-raise DOI.
> +	 */
> +	regmap_write(priv->regmap, LOONGSON_CANFD_CMD, REG_CMD_CDO);
> +
> +	return true;
> +}
> +
> +/**
> + * loongson_canfd_rx_napi_done() - Complete NAPI and re-arm the RX interrupt
> + * @napi: NAPI instance
> + * @can_complete: True if the poll loop saw an empty hardware FIFO
> + * @work_done: Frames processed during this poll
> + *
> + * The RX interrupt is re-armed only after the loop actually quiesced;
> + * a quota-limited exit keeps NAPI scheduled to poll again.
> + */
> +static void loongson_canfd_rx_napi_done(struct napi_struct *napi,
> +					bool can_complete, int work_done)
> +{
> +	struct net_device *ndev = napi->dev;
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	if (!can_complete || !napi_complete_done(napi, work_done))
> +		return;
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +		     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_RBNEI));
> +}
> +
> +/**
> + * loongson_canfd_rx_napi() - NAPI poll routine for RX
> + * @napi: NAPI structure
> + * @quota: Maximum number of frames to process
> + *
> + * Return: Number of frames processed.
> + */
> +static int loongson_canfd_rx_napi(struct napi_struct *napi, int quota)
> +{
> +	struct net_device *ndev = napi->dev;
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	bool can_complete = false;
> +	int work_done = 0;
> +	int ret;
> +
> +	while (work_done < quota) {
> +		/* FIFO empty: nothing left to consume */
> +		if (loongson_canfd_rx_is_empty_fifo(priv)) {
> +			can_complete = true;
> +			break;
> +		}
> +
> +		ret = loongson_canfd_rx_one_frame_fifo(ndev);
> +		if (ret == 1)
> +			work_done++;
> +	}
> +
> +	loongson_canfd_rx_napi_done(napi, can_complete, work_done);
> +
> +	return work_done;
> +}
> +
> +/**
> + * loongson_canfd_get_bec() - Read error counters
> + * @priv: Driver private data
> + * @bec: Structure to fill with TX/RX error counts
> + */
> +static void loongson_canfd_get_bec(struct loongson_canfd_priv *priv,
> +				   struct can_berr_counter *bec)
> +{
> +	u32 erc;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_ERC, &erc);
> +	bec->rxerr = FIELD_GET(REG_ERC_REC, erc);
> +	bec->txerr = FIELD_GET(REG_ERC_TEC, erc);
> +}
> +
> +/**
> + * loongson_canfd_get_berr_counter() - Get error counter (callback)
> + * @ndev: Network device
> + * @bec: Structure to fill
> + *
> + * Return: 0 always.
> + */
> +static int loongson_canfd_get_berr_counter(const struct net_device *ndev,
> +					   struct can_berr_counter *bec)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	loongson_canfd_get_bec(priv, bec);
> +	return 0;
> +}
> +
> +/**
> + * loongson_canfd_get_fault_state() - Read fault confinement state
> + * @ndev: Network device
> + *
> + * Return: Current CAN state (ACTIVE, WARNING, PASSIVE, BUS_OFF).
> + */
> +static enum can_state loongson_canfd_get_fault_state(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	enum can_state sts = CAN_STATE_ERROR_PASSIVE;
> +	u32 fstat, ewl, erl, rec_tec, max_tec;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_FSTAT, &fstat);
> +
> +	switch (FIELD_GET(REG_FSTAT_MASK, fstat)) {
> +	case REG_FSTAT_ERA:
> +		regmap_read(priv->regmap, LOONGSON_CANFD_ERL, &erl);
> +		regmap_read(priv->regmap, LOONGSON_CANFD_ERC, &rec_tec);
> +
> +		ewl = FIELD_GET(REG_ERL_EW, erl);
> +		max_tec = max(FIELD_GET(REG_ERC_REC, rec_tec),
> +			      FIELD_GET(REG_ERC_TEC, rec_tec));
> +
> +		if (ewl > max_tec)
> +			sts = CAN_STATE_ERROR_ACTIVE;
> +		else
> +			sts = CAN_STATE_ERROR_WARNING;
> +		break;
> +	case REG_FSTAT_ERP:
> +		sts = CAN_STATE_ERROR_PASSIVE;
> +		break;
> +	case REG_FSTAT_BOF:
> +		sts = CAN_STATE_BUS_OFF;
> +		break;
> +	default:
> +		netdev_err(ndev, "Invalid error state\n");
> +		break;
> +	}
> +
> +	return sts;
> +}
> +
> +/**
> + * loongson_canfd_report_state_change() - Report a CAN state transition
> + * @ndev: Network device
> + * @new_state: New CAN state reported by the controller
> + * @bec: Current error counters (used to fill the error frame)
> + *
> + * Called only when the protocol state actually moved: the controller
> + * latches FCSI on reset-to-Error-Active even without a visible
> + * transition, and the caller filters that case.
> + */
> +static void loongson_canfd_report_state_change(struct net_device *ndev,
> +					       enum can_state new_state,
> +					       const struct can_berr_counter *bec)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct can_frame *cf;
> +	struct sk_buff *skb;
> +
> +	netdev_info(ndev, "state changes from %s to %s\n",
> +		    can_get_state_str(priv->can.state),
> +		    can_get_state_str(new_state));
> +
> +	priv->can.state = new_state;
> +
> +	/* Stats and BUS_OFF handling must not depend on skb allocation */
> +	switch (new_state) {
> +	case CAN_STATE_BUS_OFF:
> +		priv->can.can_stats.bus_off++;
> +		can_bus_off(ndev);
> +		break;
> +	case CAN_STATE_ERROR_PASSIVE:
> +		priv->can.can_stats.error_passive++;
> +		break;
> +	case CAN_STATE_ERROR_WARNING:
> +		priv->can.can_stats.error_warning++;
> +		break;
> +	default:
> +		break;
> +	}
> +
> +	skb = alloc_can_err_skb(ndev, &cf);
> +	if (!skb)
> +		return;
> +
> +	switch (new_state) {
> +	case CAN_STATE_BUS_OFF:
> +		cf->can_id |= CAN_ERR_BUSOFF;
> +		goto send;
> +	case CAN_STATE_ERROR_ACTIVE:
> +		cf->can_id |= CAN_ERR_CNT;
> +		cf->data[1] = CAN_ERR_CRTL_ACTIVE;
> +		break;
> +	case CAN_STATE_ERROR_PASSIVE:
> +		cf->can_id |= CAN_ERR_CRTL | CAN_ERR_CNT;
> +		if (bec->rxerr >= CAN_ERROR_PASSIVE_THRESHOLD)
> +			cf->data[1] = CAN_ERR_CRTL_RX_PASSIVE;
> +		else
> +			cf->data[1] = CAN_ERR_CRTL_TX_PASSIVE;
> +		break;
> +	case CAN_STATE_ERROR_WARNING:
> +		cf->can_id |= CAN_ERR_CRTL | CAN_ERR_CNT;
> +		if (bec->txerr > bec->rxerr)
> +			cf->data[1] |= CAN_ERR_CRTL_TX_WARNING;
> +		else
> +			cf->data[1] |= CAN_ERR_CRTL_RX_WARNING;
> +		break;
> +	default:
> +		netdev_err(ndev, "Unexpected state: %d\n", new_state);
> +		kfree_skb(skb);
> +		return;
> +	}
> +
> +	cf->data[6] = bec->txerr;
> +	cf->data[7] = bec->rxerr;
> +send:
> +	netif_rx(skb);
> +}
> +
> +/**
> + * loongson_canfd_report_arb_lost() - Report an arbitration lost event
> + * @ndev: Network device
> + *
> + * Accounts the event and emits the corresponding CAN error frame.
> + */
> +static void loongson_canfd_report_arb_lost(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct can_frame *cf;
> +	struct sk_buff *skb;
> +
> +	priv->can.can_stats.arbitration_lost++;
> +
> +	skb = alloc_can_err_skb(ndev, &cf);
> +	if (!skb)
> +		return;
> +
> +	cf->can_id |= CAN_ERR_LOSTARB;
> +	cf->data[0] = CAN_ERR_LOSTARB_UNSPEC;
> +
> +	netif_rx(skb);
> +}
> +
> +/**
> + * loongson_canfd_report_bus_error() - Report a bus error event
> + * @ndev: Network device
> + *
> + * Accounts the event and emits the corresponding CAN error frame.
> + */
> +static void loongson_canfd_report_bus_error(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	struct can_frame *cf;
> +	struct sk_buff *skb;
> +
> +	priv->can.can_stats.bus_error++;
> +	scoped_guard(spinlock_irqsave, &priv->stats_lock) {
> +		stats->rx_errors++;
> +	}
> +
> +	skb = alloc_can_err_skb(ndev, &cf);
> +	if (!skb)
> +		return;
> +
> +	cf->can_id |= CAN_ERR_PROT | CAN_ERR_BUSERROR;
> +	cf->data[2] = CAN_ERR_PROT_UNSPEC;
> +	cf->data[3] = CAN_ERR_PROT_LOC_UNSPEC;
> +
> +	netif_rx(skb);
> +}
> +
> +/**
> + * loongson_canfd_err_interrupt() - Handle error interrupts
> + * @ndev: Network device
> + * @isr: Interrupt status register value
> + *
> + * Dispatches error events to their respective handlers and re-enables
> + * the serviced interrupt sources.
> + */
> +static void loongson_canfd_err_interrupt(struct net_device *ndev, u32 isr)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct can_berr_counter bec;
> +	u32 capt, alc, mask = 0;
> +	enum can_state state;
> +
> +	loongson_canfd_get_bec(priv, &bec);
> +	regmap_read(priv->regmap, LOONGSON_CANFD_ERR_CAPT, &capt);
> +	regmap_read(priv->regmap, LOONGSON_CANFD_ALC, &alc);
> +
> +	if (net_ratelimit())
> +		netdev_info(ndev, "%s: ISR 0x%08x, rxerr %d, txerr %d, error type %lu, pos %lu, ALC id_field %lu, bit %lu\n",
> +			    __func__, isr, bec.rxerr, bec.txerr,
> +			    FIELD_GET(REG_ERR_CAPT_TYPE, capt),
> +			    FIELD_GET(REG_ERR_CAPT_POS, capt),
> +			    FIELD_GET(REG_ALC_ID_FIELD, alc),
> +			    FIELD_GET(REG_ALC_BIT_POS, alc));
> +
> +	if (isr & (REG_INT_STAT_FCSI | REG_INT_STAT_EWLI)) {
> +		mask |= REG_INT_STAT_FCSI | REG_INT_STAT_EWLI;
> +		state = loongson_canfd_get_fault_state(ndev);
> +
> +		/*
> +		 * FCSI is latched on reset -> Error Active, but that is
> +		 * not a protocol-visible transition: filter it out.
> +		 */
> +		if (priv->can.state != state)
> +			loongson_canfd_report_state_change(ndev, state, &bec);
> +	}
> +
> +	if (isr & REG_INT_STAT_ALI) {
> +		mask |= REG_INT_STAT_ALI;
> +		netdev_dbg(ndev, "Arbitration lost interrupt\n");
> +		loongson_canfd_report_arb_lost(ndev);
> +	}
> +
> +	if (isr & REG_INT_STAT_BEI) {
> +		mask |= REG_INT_STAT_BEI;
> +		netdev_dbg(ndev, "Bus error interrupt\n");
> +		loongson_canfd_report_bus_error(ndev);
> +	}
> +
> +	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +		     FIELD_PREP(REG_INT_MASK_CLR, mask));
> +}
> +
> +/**
> + * loongson_canfd_tx_interrupt() - Handle TX completion interrupt
> + * @ndev: Network device
> + *
> + * For each completed TX buffer: TX_BS_VALID echoes the skb (with an
> + * optional HW TX timestamp) and counts it sent; TX_BS_FAIL and
> + * TX_BS_CANCEL free the skb as dropped. Clears the completion record
> + * per slot and wakes the queue if a buffer became free.
> + */
> +static void loongson_canfd_tx_interrupt(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	enum loongson_canfd_txbuf_sts sts;
> +	u32 tx_frame_cnt, tx_stat;
> +	bool processed = false;
> +	struct sk_buff *skb;
> +	bool wake;
> +
> +	/*
> +	 * Hold tx_lock only for the buffer scan and bookkeeping;
> +	 * netif_wake_queue() is called outside so a re-entrant
> +	 * loongson_canfd_start_xmit() on another CPU is not blocked
> +	 * on this lock.
> +	 */
> +	scoped_guard(spinlock_irqsave, &priv->tx_lock) {
> +		regmap_read(priv->regmap, LOONGSON_CANFD_TX_STAT, &tx_stat);
> +
> +		for (unsigned int buf_id = 0; buf_id < LOONGSON_CANFD_TXBUF_NUM; buf_id++) {
> +			sts = loongson_canfd_get_txbuf_sts(priv, buf_id);
> +
> +			if (sts == TX_BS_IDLE) {
> +				/* BRP set: hardware still queued/arbitrating */
> +				if (tx_stat & BIT(buf_id))
> +					continue;
> +
> +				/*
> +				 * Hardware is idle but echo_skb is occupied
> +				 * (lost interrupt). Free it, but do not issue
> +				 * SR_CLEAN on an idle slot: it can fake a
> +				 * command interrupt and cause a storm.
> +				 */
> +				if (priv->can.echo_skb[buf_id]) {
> +					netdev_warn(ndev,
> +						    "TX buf %u idle but echo_skb occupied, force free\n",
> +						    buf_id);
> +					can_free_echo_skb(ndev, buf_id, NULL);
> +					stats->tx_dropped++;
> +				}
> +				continue;
> +			}
> +
> +			processed = true;
> +			switch (sts) {
> +			case TX_BS_VALID:
> +				u32 dummy_len;
> +
> +				/*
> +				 * The controller does not latch a per-frame
> +				 * TX stamp, so the instant the handler reads
> +				 * the counter is the best estimate of the
> +				 * end of the frame.
> +				 */
> +				skb = priv->can.echo_skb[buf_id];
> +				if (skb &&
> +				    (skb_shinfo(skb)->tx_flags & SKBTX_IN_PROGRESS) &&
> +				    READ_ONCE(priv->hwts.ready))
> +					skb_hwtstamps(skb)->hwtstamp =
> +							loongson_canfd_hwts_now(priv);
> +
> +				dummy_len = can_get_echo_skb(ndev, buf_id, NULL);
> +				(void)dummy_len;
> +				stats->tx_packets++;
> +				break;
> +			case TX_BS_FAIL:
> +				regmap_read(priv->regmap, LOONGSON_CANFD_TX_FR_CNT,
> +					    &tx_frame_cnt);
> +				netdev_dbg(ndev, "TX buffer in FAIL state, TX frame count: %d\n",
> +					   tx_frame_cnt);
> +				can_free_echo_skb(ndev, buf_id, NULL);
> +				stats->tx_dropped++;
> +				break;
> +			case TX_BS_CANCEL:
> +				netdev_dbg(ndev, "TX buffer in CANCEL state\n");
> +				can_free_echo_skb(ndev, buf_id, NULL);
> +				stats->tx_dropped++;
> +				break;
> +			default:
> +				netdev_dbg(ndev, "Unexpected TX status: %d\n", sts);
> +				break;
> +			}
> +
> +			/* Clear the completion record only for finished slots */
> +			if (loongson_canfd_set_txbuf_cmd(ndev, TXT_CMD_SR_CLEAN, buf_id))
> +				netdev_warn(ndev, "Failed to clear TX buffer %u status\n", buf_id);
> +		}
> +
> +		if (!processed)
> +			netdev_warn_once(ndev, "Spurious TX interrupt\n");
> +
> +		wake = loongson_canfd_txbuf_freed(priv);
> +	}
> +
> +	if (wake)
> +		netif_wake_queue(ndev);
> +}
> +
> +/**
> + * loongson_canfd_interrupt() - Main interrupt handler
> + * @irq: IRQ number
> + * @dev_id: Device identifier (net_device)
> + *
> + * Return: IRQ_HANDLED if interrupt was handled, IRQ_NONE otherwise.
> + */
> +static irqreturn_t loongson_canfd_interrupt(int irq, void *dev_id)
> +{
> +	struct net_device *ndev = (struct net_device *)dev_id;
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	u32 isr, imask;
> +
> +	for (unsigned int loops = 0; loops < LOONGSON_CANFD_IRQ_LOOP_MAX; loops++) {
> +		regmap_read(priv->regmap, LOONGSON_CANFD_INT_STAT, &isr);
> +		if (!isr) {
> +			if (loops)
> +				return IRQ_HANDLED;
> +			else
> +				return IRQ_NONE;
> +		}
> +
> +		if (isr & REG_INT_STAT_RBNEI) {
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +				     REG_INT_STAT_RBNEI);
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
> +				     REG_INT_STAT_RBNEI);
> +
> +			napi_schedule(&priv->napi);
> +		}
> +
> +		if (isr & REG_INT_STAT_TXBHCI) {
> +			/*
> +			 * Mask-clear-handle-unmask: a TX completing during
> +			 * handling re-latches TXBHCI and fires a fresh IRQ
> +			 * after the unmask, instead of being wiped by the
> +			 * W1C in tx_interrupt().
> +			 */
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +				     REG_INT_STAT_TXBHCI);
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
> +				     REG_INT_STAT_TXBHCI);
> +			loongson_canfd_tx_interrupt(ndev);
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +				     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_TXBHCI));
> +		}
> +
> +		if (isr & REG_INT_STAT_DOI) {
> +			/* Same discipline as TXBHCI, so an overrun during
> +			 * handling is counted rather than absorbed by the
> +			 * W1C below.
> +			 */
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +				     REG_INT_STAT_DOI);
> +
> +			loongson_canfd_rx_overrun_event(ndev);
> +
> +			/* DOR is clear now: acknowledge DOI safely */
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
> +				     REG_INT_STAT_DOI);
> +
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> +				     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_DOI));
> +		}
> +
> +		imask = isr & REG_INT_STAT_ERRORI;
> +		if (imask) {
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, imask);
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT, imask);
> +			loongson_canfd_err_interrupt(ndev, isr);
> +		}
> +	}
> +
> +	netdev_err(ndev, "Interrupt loop timeout, state: 0x%x\n", isr);
> +
> +	if (isr & REG_INT_STAT_TXBHCI) {
> +		for (unsigned int i = 0; i < LOONGSON_CANFD_TXBUF_NUM; i++) {
> +			u32 sts = loongson_canfd_get_txbuf_sts(priv, i);
> +
> +			netdev_err(ndev, "TX buf %d status: 0x%x\n", i, sts);
> +		}
> +	}
> +
> +	loongson_canfd_disable_interrupts(priv);
> +
> +	return IRQ_HANDLED;
> +}
> +
> +/**
> + * loongson_canfd_start_xmit() - Transmit a CAN frame
> + * @skb: Socket buffer
> + * @ndev: Network device
> + *
> + * Return: NETDEV_TX_OK on success, NETDEV_TX_BUSY if queue full.
> + */
> +static netdev_tx_t loongson_canfd_start_xmit(struct sk_buff *skb, struct net_device *ndev)
> +{
> +	struct canfd_frame *cf = (struct canfd_frame *)skb->data;
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	int buf_id, ret;
> +
> +	if (can_dropped_invalid_skb(ndev, skb))
> +		return NETDEV_TX_OK;
> +
> +	guard(spinlock_irqsave)(&priv->tx_lock);
> +
> +	if (unlikely(!loongson_canfd_txbuf_freed(priv))) {
> +		netif_stop_queue(ndev);
> +		netdev_dbg(ndev, "No free TX buffer while queue was awake!\n");
> +		return NETDEV_TX_BUSY;
> +	}
> +
> +	buf_id = loongson_canfd_find_free_txbuf(priv);
> +	if (buf_id < 0) {
> +		netif_stop_queue(ndev);
> +		netdev_dbg(ndev, "No writable TX buffer, stopping queue\n");
> +		return NETDEV_TX_BUSY;
> +	}
> +
> +	ret = loongson_canfd_insert_frame(priv, cf, buf_id, can_is_canfd_skb(skb));
> +	if (ret) {
> +		netdev_err(ndev, "Failed to insert frame into TX buffer: %d\n", ret);
> +		dev_kfree_skb_any(skb);
> +		ndev->stats.tx_dropped++;
> +		return NETDEV_TX_OK;
> +	}
> +
> +	can_put_echo_skb(skb, ndev, buf_id, 0);
> +
> +	if (!(cf->can_id & CAN_RTR_FLAG))
> +		stats->tx_bytes += cf->len;
> +
> +	ret = loongson_canfd_set_txbuf_cmd(ndev, TXT_CMD_ADD, buf_id);
> +	if (ret) {
> +		netdev_err(ndev, "Failed to start TX buffer %d: %d\n",
> +			   buf_id, ret);
> +		can_free_echo_skb(ndev, buf_id, NULL);
> +		ndev->stats.tx_dropped++;
> +		return NETDEV_TX_OK;
> +	}
> +
> +	if (!loongson_canfd_txbuf_freed(priv))
> +		netif_stop_queue(ndev);
> +
> +	return NETDEV_TX_OK;
> +}
> +
> +/**
> + * loongson_canfd_open() - Open the network device
> + * @ndev: Network device
> + *
> + * Return: 0 on success, negative error on failure.
> + */
> +static int loongson_canfd_open(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	int ret;
> +
> +	loongson_canfd_reset(ndev);
> +
> +	ret = open_candev(ndev);
> +	if (ret) {
> +		netdev_warn(ndev, "open_candev failed\n");
> +		return ret;
> +	}
> +
> +	napi_enable(&priv->napi);
> +	ret = request_irq(ndev->irq, loongson_canfd_interrupt,
> +			  IRQF_SHARED, ndev->name, ndev);
> +	if (ret < 0) {
> +		netdev_err(ndev, "IRQ request failed\n");
> +		goto exit_napi_disable;
> +	}
> +
> +	ret = loongson_canfd_chip_start(ndev);
> +	if (ret < 0) {
> +		netdev_err(ndev, "Chip start failed\n");
> +		goto exit_free_irq;
> +	}
> +
> +	netdev_info(ndev, "Loongson CANFD device registered\n");
> +	netif_start_queue(ndev);
> +
> +	return 0;
> +
> +exit_free_irq:
> +	free_irq(ndev->irq, ndev);
> +exit_napi_disable:
> +	napi_disable(&priv->napi);
> +	close_candev(ndev);
> +	return ret;
> +}
> +
> +/**
> + * loongson_canfd_close() - Close the network device
> + * @ndev: Network device
> + *
> + * Cancels a pending bus-off restart before tearing down IRQ/DMA, since
> + * loongson_canfd_do_set_mode() re-arms both.
> + *
> + * Return: 0 always.
> + */
> +static int loongson_canfd_close(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	netif_stop_queue(ndev);
> +	napi_disable(&priv->napi);
> +
> +	/*
> +	 * Cancel a pending bus-off restart before tearing down IRQ/DMA:
> +	 * do_set_mode() re-arms both, so a restart firing after
> +	 * free_irq() would touch released resources. close_candev()
> +	 * does the same but only after free_irq().
> +	 */
> +	cancel_delayed_work_sync(&priv->can.restart_work);
> +
> +	loongson_canfd_chip_stop(ndev);
> +	free_irq(ndev->irq, ndev);
> +	close_candev(ndev);
> +
> +	return 0;
> +}
> +
> +static const struct net_device_ops loongson_canfd_netdev_ops = {
> +	.ndo_open           = loongson_canfd_open,
> +	.ndo_stop           = loongson_canfd_close,
> +	.ndo_start_xmit     = loongson_canfd_start_xmit,
> +	.ndo_hwtstamp_get   = can_hwtstamp_get,
> +	.ndo_hwtstamp_set   = can_hwtstamp_set,
> +};
> +
> +/**
> + * loongson_canfd_get_ts_info() - Report timestamping capabilities
> + * @ndev: Network device
> + * @info: ethtool timestamping info to fill
> + *
> + * Hardware timestamping is advertised only while @hwts.ready is set,
> + * so the reported capabilities match the driver's actual state.
> + *
> + * Return: 0 always.
> + */
> +static int loongson_canfd_get_ts_info(struct net_device *ndev,
> +				      struct kernel_ethtool_ts_info *info)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	info->so_timestamping = SOF_TIMESTAMPING_SOFTWARE |
> +				SOF_TIMESTAMPING_TX_SOFTWARE |
> +				SOF_TIMESTAMPING_RX_SOFTWARE;
> +
> +	info->phc_index = -1;
> +
> +	if (READ_ONCE(priv->hwts.ready)) {
> +		info->so_timestamping |= SOF_TIMESTAMPING_TX_HARDWARE |
> +					 SOF_TIMESTAMPING_RX_HARDWARE |
> +					 SOF_TIMESTAMPING_RAW_HARDWARE;
> +		info->tx_types = BIT(HWTSTAMP_TX_ON);
> +		info->rx_filters = BIT(HWTSTAMP_FILTER_ALL);
> +	}
> +
> +	return 0;
> +}
> +
> +static const struct ethtool_ops loongson_canfd_ethtool_ops = {
> +	.get_ts_info = loongson_canfd_get_ts_info,
> +};
> +
> +static const struct regmap_range loongson_canfd_reg_table_wr_range[] = {
> +	regmap_reg_range(LOONGSON_CANFD_DEVICE_ID, LOONGSON_CANFD_CONF),
> +	regmap_reg_range(LOONGSON_CANFD_CMD, LOONGSON_CANFD_CMD),
> +	regmap_reg_range(LOONGSON_CANFD_INT_STAT, LOONGSON_CANFD_ERL),
> +	regmap_reg_range(LOONGSON_CANFD_CTR_PRES, LOONGSON_CANFD_CTR_PRES),
> +	regmap_reg_range(LOONGSON_CANFD_SSP_CFG, LOONGSON_CANFD_SSP_CFG),
> +	regmap_reg_range(LOONGSON_CANFD_TS, LOONGSON_CANFD_FLT_CTRL),
> +	regmap_reg_range(LOONGSON_CANFD_TX_CMD, LOONGSON_CANFD_TX_DATA_18),
> +};
> +
> +static const struct regmap_range loongson_canfd_reg_table_rd_range[] = {
> +	regmap_reg_range(LOONGSON_CANFD_DEVICE_ID, LOONGSON_CANFD_STAT),
> +	regmap_reg_range(LOONGSON_CANFD_INT_STAT, LOONGSON_CANFD_BRE),
> +	regmap_reg_range(LOONGSON_CANFD_ERR_CAPT, LOONGSON_CANFD_TX_STAT),
> +	regmap_reg_range(LOONGSON_CANFD_TX_SEL, LOONGSON_CANFD_TX_DATA_18),
> +};
> +
> +static const struct regmap_access_table loongson_canfd_reg_table_wr = {
> +	.yes_ranges = loongson_canfd_reg_table_wr_range,
> +	.n_yes_ranges = ARRAY_SIZE(loongson_canfd_reg_table_wr_range),
> +};
> +
> +static const struct regmap_access_table loongson_canfd_reg_table_rd = {
> +	.yes_ranges = loongson_canfd_reg_table_rd_range,
> +	.n_yes_ranges = ARRAY_SIZE(loongson_canfd_reg_table_rd_range),
> +};
> +
> +static bool loongson_canfd_volatile_reg(struct device *dev, unsigned int reg)
> +{
> +	switch (reg) {
> +	case LOONGSON_CANFD_MODE:
> +	case LOONGSON_CANFD_CONF:
> +	case LOONGSON_CANFD_STAT:
> +	case LOONGSON_CANFD_CMD:
> +	case LOONGSON_CANFD_INT_STAT:
> +	case LOONGSON_CANFD_INT_ENA:
> +	case LOONGSON_CANFD_INT_MASK:
> +	case LOONGSON_CANFD_ERL:
> +	case LOONGSON_CANFD_FSTAT:
> +	case LOONGSON_CANFD_ERC:
> +	case LOONGSON_CANFD_ERR_CAPT:
> +	case LOONGSON_CANFD_ALC:
> +	case LOONGSON_CANFD_TRV_DLY:
> +	case LOONGSON_CANFD_TX_FR_CNT:
> +	case LOONGSON_CANFD_TS:
> +	case LOONGSON_CANFD_RX_STAT:
> +	case LOONGSON_CANFD_RX_DATA:
> +	case LOONGSON_CANFD_TX_STAT:
> +	case LOONGSON_CANFD_TX_CMD:
> +	case LOONGSON_CANFD_TX_SEL:
> +		return true;
> +	default:
> +		return false;
> +	};
> +}
> +
> +static const struct regmap_config loongson_canfd_regmap = {
> +	.reg_bits	= 32,
> +	.reg_stride	= 4,
> +	.val_bits	= 32,
> +	.wr_table	= &loongson_canfd_reg_table_wr,
> +	.rd_table	= &loongson_canfd_reg_table_rd,
> +	.volatile_reg	= loongson_canfd_volatile_reg,
> +	.max_register	= LOONGSON_CANFD_TX_DATA_18,
> +	.cache_type	= REGCACHE_MAPLE,
> +};
> +
> +/**
> + * loongson_canfd_setup_can_priv() - Fill in can_priv static configuration
> + * @ndev: Network device
> + * @clk_rate: Controller input clock, in Hz
> + *
> + * The whole can_priv configuration is data-only; keeping it out of
> + * probe() makes the resource-acquisition flow easier to follow.
> + */
> +static void loongson_canfd_setup_can_priv(struct net_device *ndev, u32 clk_rate)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	priv->can.clock.freq = clk_rate;
> +	priv->can.bittiming_const = &loongson_canfd_bit_timing;
> +	priv->can.fd.data_bittiming_const = &loongson_canfd_bit_timing_data;
> +	priv->can.fd.tdc_const = &loongson_canfd_tdc_data;
> +
> +	priv->can.fd.do_set_data_bittiming = loongson_canfd_set_data_bittiming;
> +	priv->can.fd.do_get_auto_tdcv = loongson_canfd_get_auto_tdcv;
> +	priv->can.do_set_mode = loongson_canfd_do_set_mode;
> +	priv->can.do_set_bittiming = loongson_canfd_set_bittiming;
> +	priv->can.do_get_berr_counter = loongson_canfd_get_berr_counter;
> +
> +	priv->can.ctrlmode_supported = CAN_CTRLMODE_LOOPBACK |
> +				       CAN_CTRLMODE_LISTENONLY |
> +				       CAN_CTRLMODE_ONE_SHOT |
> +				       CAN_CTRLMODE_BERR_REPORTING |
> +				       CAN_CTRLMODE_FD |
> +				       CAN_CTRLMODE_PRESUME_ACK |
> +				       CAN_CTRLMODE_FD_NON_ISO |
> +				       CAN_CTRLMODE_TDC_AUTO |
> +				       CAN_CTRLMODE_CC_LEN8_DLC;
> +}
> +
> +static int loongson_canfd_probe(struct platform_device *pdev)
> +{
> +	struct loongson_canfd_priv *priv;
> +	struct device *dev = &pdev->dev;
> +	struct resource *mem_res;
> +	struct net_device *ndev;
> +	struct regmap *regmap;
> +	void __iomem *base;
> +	int ret, irq;
> +	u32 clk_rate;
> +
> +	base = devm_platform_get_and_ioremap_resource(pdev, 0, &mem_res);
> +	if (IS_ERR(base))
> +		return PTR_ERR(base);
> +
> +	regmap = devm_regmap_init_mmio(dev, base, &loongson_canfd_regmap);
> +	if (IS_ERR(regmap))
> +		return PTR_ERR(regmap);
> +
> +	ret = device_property_read_u32(dev, "clock-frequency", &clk_rate);
> +	if (ret)
> +		return dev_err_probe(dev, ret, "Missing clock-frequency property");
> +
> +	irq = platform_get_irq(pdev, 0);
> +	if (irq < 0)
> +		return dev_err_probe(dev, irq, "Failed to get IRQ");
> +
> +	ndev = alloc_candev(sizeof(*priv), LOONGSON_CANFD_TXBUF_NUM);
> +	if (!ndev)
> +		return -ENOMEM;
> +
> +	priv = netdev_priv(ndev);
> +	spin_lock_init(&priv->tx_lock);
> +	spin_lock_init(&priv->stats_lock);
> +	spin_lock_init(&priv->hwts.lock);
> +	priv->regmap = regmap;
> +	priv->mem_res = mem_res;
> +
> +	loongson_canfd_setup_can_priv(ndev, clk_rate);
> +
> +	ndev->irq = irq;
> +	ndev->flags |= IFF_ECHO;
> +	platform_set_drvdata(pdev, ndev);
> +	ndev->netdev_ops = &loongson_canfd_netdev_ops;
> +	ndev->ethtool_ops = &loongson_canfd_ethtool_ops;
> +	SET_NETDEV_DEV(ndev, dev);
> +
> +	netif_napi_add(ndev, &priv->napi, loongson_canfd_rx_napi);
> +
> +	ret = register_candev(ndev);
> +	if (ret) {
> +		dev_err(dev, "register_candev failed with %d", ret);
> +		goto err_candev_free;
> +	}
> +
> +	return 0;
> +
> +err_candev_free:
> +	netif_napi_del(&priv->napi);
> +	free_candev(ndev);
> +	return ret;
> +}
> +
> +/**
> + * loongson_canfd_remove() - Remove the device
> + * @pdev: Platform device
> + */
> +static void loongson_canfd_remove(struct platform_device *pdev)
> +{
> +	struct net_device *ndev = platform_get_drvdata(pdev);
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +
> +	unregister_candev(ndev);
> +	netif_napi_del(&priv->napi);
> +	free_candev(ndev);
> +}
> +
> +static const struct acpi_device_id loongson_canfd_acpi_match[] = {
> +	{ .id = "LOON0015" },
> +	{ }
> +};
> +MODULE_DEVICE_TABLE(acpi, loongson_canfd_acpi_match);
> +
> +static struct platform_driver loongson_canfd_driver = {
> +	.probe  = loongson_canfd_probe,
> +	.remove = loongson_canfd_remove,
> +	.driver = {
> +		.name = DEV_NAME,
> +		.acpi_match_table = loongson_canfd_acpi_match,
> +	},
> +};
> +module_platform_driver(loongson_canfd_driver);
> +
> +MODULE_AUTHOR("Loongson Technology Corporation Limited");
> +MODULE_DESCRIPTION("Loongson CAN-FD Controller driver");
> +MODULE_LICENSE("GPL");

[Severity: Medium]
The regmap uses REGCACHE_MAPLE, but every register the driver reads
back is listed in loongson_canfd_volatile_reg(), and the cache is
never synced or invalidated. What does the cache provide here?

As configured, it has side effects. The TX buffer window
LOONGSON_CANFD_TX_DATA_1..LOONGSON_CANFD_TX_DATA_18 is writable and
readable but not volatile. Each regmap_write() from
loongson_canfd_insert_frame() therefore goes through
regcache_maple_write() before the MMIO write.

That code runs in loongson_canfd_start_xmit() under tx_lock. The
first write to each offset allocates maple nodes and an entry array
with GFP_ATOMIC, because the MMIO regmap is fast_io. If either
allocation fails, regmap_write() returns -ENOMEM without touching the
hardware, and the frame is dropped with a netdev_err() that is not
rate-limited.

The window is also banked through LOONGSON_CANFD_TX_SEL, so the cache
folds the eight hardware TX buffers into one set of cached values. The
driver never reads them back today. Any later read or
regmap_update_bits() on that range would return another buffer's data.

Several readable counter and status registers are not volatile either:
LOONGSON_CANFD_BRE, LOONGSON_CANFD_RETX_CNT, LOONGSON_CANFD_RX_FR_CNT,
LOONGSON_CANFD_DEBUG, LOONGSON_CANFD_RX_MEM_INFO and
LOONGSON_CANFD_RX_PRT. After the first read they are served from the
cache, so the regmap debugfs registers file shows frozen values.

LOONGSON_CANFD_RX_DATA is readable but has no precious_reg callback.
Reading the debugfs registers file therefore pops a word from the live
RX FIFO. That desynchronizes the header parsing in
loongson_canfd_rx_hdr_valid() for the frame being received.

Would REGCACHE_NONE be simpler here? If the cache is wanted, the TX
window and the counters should be volatile, and RX_DATA should be
marked precious.

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1794213427.git.zhoubinbin%40loongson.cn

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support
  2026-11-09  8:42 ` [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support Binbin Zhou
  2026-11-09  9:51   ` sashiko-bot
@ 2026-11-10 22:20   ` netdev-bot+sashiko
  1 sibling, 0 replies; 7+ messages in thread
From: netdev-bot+sashiko @ 2026-11-10 22:20 UTC (permalink / raw)
  To: zhoubinbin
  Cc: zhoubb.aaron, chenhuacai, mkl, mailhol, libingxiong, chenhuacai,
	kernel, loongarch, linux-can, jeffbai, kuba

Thank you for your contribution! Sashiko AI review found 24 potential
issue(s) to consider.

Critical: 0 · High: 5 · Medium: 10 · Low: 9

If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).

- [High] In DMA mode, the skb-allocation-failure path of
  loongson_canfd_rx_one_frame_dma() sets rx_rd_pos = saved_rd_pos + 1.
- [High] loongson_canfd_rxdma_start() only checks the result of
  dmaengine_prep_dma_cyclic() for NULL.
- [High] loongson_canfd_rx_is_empty_dma() never returns true in the
  normal drained state (rx_wr_pos == rx_rd_pos, FIFO idle), because…
- [High] loongson_canfd_rx_napi_done() now updates the DMA ring
  bookkeeping after napi_complete_done() has released NAPI ownership.
- [High] can: loongson_canfd: full-ring discard in
  loongson_canfd_rx_is_empty_dma()
- [Medium] loongson_canfd_dma_sync_wr() derives progress from the residue
  modulo the ring size.
- [Medium] The DMA RX path relies on dmaengine_tx_status() returning an
  exact, burst-granular, monotonic in-flight residue for the cyclic…
- [Medium] Nothing recovers RX if the DMA channel stops moving data.
- [Medium] loongson_canfd_do_set_mode(CAN_MODE_START) is reached from
  can_restart() via the restart work or a netlink restart.
- [Medium] When the reader loses frame alignment, the resync heuristics
  accept any word pair that passes loongson_canfd_hdr_plausible() as a…
- [Medium] The cyclic RX descriptor uses buf_len 32 KiB and period_len 4
  KiB.
- [Medium] The start and restart sequences do not match the driver's own
  DMA model.
- [Medium] REG_INT_STAT_DMADI is defined as 'Message DMA Transfer
  Complete Interrupt', a per-message CAN-controller event.
- [Medium] The RX ring is allocated with dma_alloc_coherent() on the CAN
  client device (ndev->dev.parent).
- [Medium] can: loongson_canfd: wrong check order in
  loongson_canfd_rx_napi_done()
- [Low] loongson_canfd_rx_overrun_event() now has a second caller,
  rx_napi_done(), which runs in softirq after napi_complete_done(), in…
- [Low] The patch is inconsistent about locking ndev->stats.rx_dropped.
- [Low] Several new comments contradict the code or each other: - The
  kernel-doc added to loongson_canfd_tx_interrupt() says 'RBNEI is…
- [Low] The commit message presents RX DMA as optional and says the
  driver falls back to FIFO polling, which implies FIFO mode is…
- [Low] The commit message claims DMA mode reduces MMIO accesses and
  improves RX throughput, with no measurements.
- [Low] Documentation does not match the code for rx_bad_cnt and
  -ENODATA.
- [Low] loongson_canfd_rx_one_frame_dma_overrun() documents an @avail
  parameter that it never uses.
- [Low] The new DMA RX path logs 'RX DMA overrun, resyncing'
  (rx_one_frame_dma_overrun()) and 'RX pointer inversion'…
- [Low] FIFO mode reads RX words through regmap-mmio, which by default
  uses readl() and converts from little-endian.

--

can: loongson_canfd: Add RXDMA support

This patch adds optional RX DMA support to the Loongson CAN-FD driver.
When an "rx" DMA channel is present, a cyclic DMA transfer moves frames
into a 32 KiB coherent ring, and the driver parses them from there
through new per-mode callbacks. Otherwise it uses the existing FIFO poll
path. The patch also reworks NAPI completion and adds handling for the
DMADI interrupt.

> When an "rx" DMA channel is available, incoming frames are moved into
> a ring buffer by a cyclic DMA transfer and read directly from there,
> reducing MMIO accesses and improving RX throughput under high traffic.

[Severity: Low]
Is the "reducing MMIO accesses" claim accurate for classic CAN frames?
Counting per frame in DMA mode:

loongson_canfd_rx_napi()
  loongson_canfd_rx_is_empty_dma()
    loongson_canfd_dma_sync_wr() -> dmaengine_tx_status()
        (provider spinlock, CCR and CNDTR reads)
    regmap_read(RX_STAT)
  loongson_canfd_rx_one_frame_dma()
    loongson_canfd_dma_sync_wr() -> dmaengine_tx_status()
        (spinlock, two more MMIO reads)

That comes to about five MMIO reads and two spinlock round trips per
frame. An 8-byte classic frame in FIFO mode costs about the same: RX_STAT,
two header words and two payload words. DMADI also adds a per-message
interrupt.

Are there measurements that could go into the commit message?

> The DMA path is fully transparent to the networking stack; the driver
> falls back to FIFO polling when no DMA channel is present.

[Severity: Low]
This reads as if FIFO mode is unchanged. However,
loongson_canfd_rx_napi_done() is rewritten for both modes.

With rx_ch == NULL it now calls loongson_canfd_rx_overrun_event() from
NAPI context and does an extra RX_STAT read. When RXFRC != 0 or RXE is
clear, it calls napi_schedule() instead of unmasking RBNEI. Before this
patch it only unmasked RBNEI.

Could the commit message mention this change to the FIFO path?

> diff --git a/drivers/net/can/loongson_canfd.c b/drivers/net/can/loongson_canfd.c
> index 7a4d8df7815ac..b5a5917bc7738 100644
> --- a/drivers/net/can/loongson_canfd.c
> +++ b/drivers/net/can/loongson_canfd.c

[ ... ]

> @@ -409,6 +418,17 @@ struct loongson_canfd_rx_desc {
>   * @tx_lock: Lock for synchronizing TX interrupt handling
>   * @stats_lock: Lock for protecting netdev statistics
>   * @hwts: Hardware timestamp state
> + * @rx_ch: DMA receive channel, NULL in poll mode
> + * @rx_cookie: DMA receive cookie
> + * @rx_dma_addr: DMA RX buffer bus address
> + * @rx_cpu_buf: DMA RX buffer CPU address
> + * @rx_rd_pos: Software read position, 64-bit monotonic, in 32-bit words
> + * @rx_wr_pos: DMA write position, 64-bit monotonic, wrap-extended
> + * @rx_wr_off: Last raw residue-derived ring offset, 0..RXDMA_NUM-1
> + * @rx_bad_cnt: Consecutive invalid RX headers seen in DMA mode

[Severity: Low]
This isn't a bug, but the description doesn't match the code.
rx_bad_cnt is only incremented after loongson_canfd_rx_deliver() returns
-ENOMEM, and every invalid-header path resets it to 0.

The same goes for loongson_canfd_rx_one_frame_dma(). Its kernel-doc says
-ENODATA means "a run of malformed frames was detected", but it is
returned for a single zero or unresolvable header. Its -ENOMEM path also
returns 1 ("one frame consumed") while advancing only one word.

Could the documentation be updated to match?

[ ... ]

> @@ -504,6 +539,213 @@ static void loongson_canfd_disable_interrupts(struct loongson_canfd_priv *priv)
>  	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK, REG_INT_MASK_SET);
>  }
>
> +/**
> + * loongson_canfd_dma_sync_wr() - Refresh the 64-bit DMA write position

[ ... ]

> +static void loongson_canfd_dma_sync_wr(struct loongson_canfd_priv *priv)
> +{
> +	struct dma_tx_state state;
> +	u32 off;
> +
> +	if (!priv->rx_ch)
> +		return;
> +
> +	if (dmaengine_tx_status(priv->rx_ch, priv->rx_cookie, &state) !=
> +	    DMA_IN_PROGRESS)
> +		return;

[Severity: Medium]
Any status other than DMA_IN_PROGRESS returns silently, so a channel
fault looks the same as "no progress yet". For example, the CMC provider
only logs a TEI with dev_err() and never completes the cookie.

If the channel stops moving data, nothing reads RX_DATA over MMIO in DMA
mode. The FIFO then stays non-empty while rx_wr_pos stays frozen:

loongson_canfd_rx_is_empty_dma()    returns false (RXFRC > 0)
loongson_canfd_rx_one_frame_dma()   returns -EAGAIN
loongson_canfd_rx_napi_done()
	/* FIFO still holds data: keep NAPI scheduled, RBNEI stays masked */
	napi_schedule(napi);

Is there a way to recover RX in this state? As written, NAPI seems to
reschedule itself forever on one CPU.

> +
> +	if (state.residue > LOONGSON_CANFD_RXBUF_SZ)
> +		state.residue = LOONGSON_CANFD_RXBUF_SZ;
> +
> +	off = (LOONGSON_CANFD_RXBUF_SZ - state.residue) /
> +	      DMA_SLAVE_BUSWIDTH_4_BYTES;
> +	off %= LOONGSON_CANFD_RXDMA_NUM;

[Severity: Medium]
Each sample can only add between 0 and RXDMA_NUM - 1 words. Suppose the
cyclic DMA writes RXDMA_NUM words or more between two samples, for
example because NAPI was delayed by about 32 KiB of bus traffic. Then
rx_wr_pos is undercounted by a multiple of the ring size, and exactly one
lap looks like no progress at all.

In that case, can the avail > LOONGSON_CANFD_RXDMA_NUM overrun check in
loongson_canfd_rx_one_frame_dma() ever fire? Without it, overwritten slots
get parsed with the wrong frame alignment.

When an overrun is detected, loongson_canfd_rx_one_frame_dma_overrun()
does:

	priv->rx_rd_pos = priv->rx_wr_pos - LOONGSON_CANFD_RXDMA_NUM + 2;

That puts the reader only two words ahead of the next DMA write slot, and
not on a frame boundary.

> +
> +	if (off < priv->rx_wr_off)
> +		/* The DMA engine wrapped around the end of the ring. */
> +		priv->rx_wr_pos += LOONGSON_CANFD_RXDMA_NUM -
> +				   priv->rx_wr_off + off;
> +	else
> +		priv->rx_wr_pos += off - priv->rx_wr_off;
> +
> +	priv->rx_wr_off = off;
> +}

[Severity: Medium]
This treats any backward step of off as a full wrap, which assumes the
residue is exact and monotonic.

loongson2-apb-cmc-dma never sets ddev->residue_granularity, so it reports
DMA_RESIDUE_GRANULARITY_DESCRIPTOR. loongson_canfd_rxdma_setup() doesn't
check dma_get_slave_caps() before choosing DMA mode.

The provider's residue also mixes the live CNDTR with the software
next_sg, and next_sg only advances in its IRQ handler:

	ndtr = loongson2_cmc_dma_read(lddev, LOONGSON2_CMCDMA_CNDTR, lchan->id);
	residue = ndtr << width;
	...
	for (i = next_sg; i < desc->num_sgs; i++)
		residue += desc->sg_req[i].len;

If CNDTR reloads at terminal count, a sample taken between TC and the IRQ
comes out one period too large. Would that be treated as a wrap here,
pushing rx_wr_pos ahead by about 7168 words?

> +
> +/**
> + * loongson_canfd_read_rx_word_dma() - Read one word from the DMA buffer
> + * @priv: Driver private data
> + *
> + * Consumes the next word from the cyclic ring and advances rx_rd_pos.
> + *
> + * Return: The next 32-bit data word.
> + */
> +static u32 loongson_canfd_read_rx_word_dma(struct loongson_canfd_priv *priv)
> +{
> +	u32 idx = priv->rx_rd_pos % LOONGSON_CANFD_RXDMA_NUM;
> +
> +	priv->rx_rd_pos++;
> +	return priv->rx_cpu_buf[idx];
> +}

[Severity: Low]
FIFO mode reads RX_DATA through regmap-mmio, which uses readl() and
converts from little-endian. Here the same words come from the ring as a
native unsigned int, with no le32_to_cpu().

On a big-endian kernel, would the DMA path see byte-swapped headers and
payload compared with the FIFO path? CAN_LOONGSON_CANFD only depends on
HAS_IOMEM.

The same applies to loongson_canfd_ring_peek() and the direct
rx_cpu_buf[] reads in loongson_canfd_rx_is_empty_dma().

[ ... ]

> +static int loongson_canfd_rxdma_init(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct device *dev = ndev->dev.parent;
> +	struct dma_slave_config config;
> +	int ret;
> +
> +	if (!priv->rx_ch)
> +		return -ENODEV;
> +
> +	priv->rx_cpu_buf = dma_alloc_coherent(dev, LOONGSON_CANFD_RXBUF_SZ,
> +					      &priv->rx_dma_addr, GFP_KERNEL);

[Severity: Medium]
Should this buffer be allocated against the DMA controller, using
dmaengine_get_dma_device(priv->rx_ch), rather than ndev->dev.parent?

The CMC engine is the device that writes the ring, and its CMAR/CPAR
registers are only 32 bits wide. The driver never limits the DMA mask to
match. If the CAN node's DMA mask, dma-ranges or IOMMU setup differs from
the controller's, could the engine write to a truncated or wrong address?

loongson_canfd_rxdma_free() would need the same change.

[ ... ]

> +static int loongson_canfd_rxdma_start(struct net_device *ndev)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct dma_async_tx_descriptor *desc;
> +
> +	if (!priv->rx_ch)
> +		return 0;
> +
> +	priv->rx_rd_pos = 0;
> +	priv->rx_wr_pos = 0;
> +	priv->rx_wr_off = 0;
> +	priv->rx_bad_cnt = 0;
> +
> +	desc = dmaengine_prep_dma_cyclic(priv->rx_ch, priv->rx_dma_addr,
> +					 LOONGSON_CANFD_RXBUF_SZ,
> +					 LOONGSON_CANFD_RXDMA_PERIOD,
> +					 DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT);

[Severity: Medium]
With buf_len at 32 KiB and period_len at 4 KiB, loongson2-apb-cmc-dma
doesn't use hardware circular mode, because it only sets CCR_CIRC when
buf_len == period_len. Instead it builds 8 sg entries and advances them
from its channel IRQ handler: it clears CCR_EN, writes CMAR, then sets
CCR_EN again.

So at every 4 KiB boundary, for the whole TC-to-IRQ latency, the channel
either isn't servicing the CAN DMA request or is still writing to the old
CMAR. The RX FIFO is only 80 bytes. Can one FD frame arriving in that gap
overflow it?

No callback is installed, so the per-period DMA_PREP_INTERRUPT doesn't
seem to be needed. Would a single 8192-item period, which is under the
64K item limit, select true circular mode instead?

> +	if (!desc) {
> +		netdev_err(ndev, "RX DMA cyclic prep failed\n");
> +		return -EBUSY;
> +	}
> +
> +	priv->rx_cookie = dmaengine_submit(desc);

[Severity: High]
Can desc be an ERR_PTR here? loongson2_cmc_dma_prep_dma_cyclic() is the
provider named in MODULE_SOFTDEP, and it returns ERR_PTR on failure:

	desc = kzalloc_flex(*desc, sg_req, num_periods, GFP_NOWAIT);
	if (!desc)
		return ERR_PTR(-ENOMEM);

An ERR_PTR passes the !desc check, and dmaengine_submit() then
dereferences desc->tx_submit. This path runs from ndo_open, and from
do_set_mode(CAN_MODE_START) in the bus-off restart work.

Would IS_ERR_OR_NULL() be the right check here?

> +	if (dma_submit_error(priv->rx_cookie)) {
> +		netdev_err(ndev, "RX DMA submit failed\n");
> +		return -EIO;
> +	}
> +
> +	dma_async_issue_pending(priv->rx_ch);
> +
> +	/* Skip zero-fill produced before the controller is enabled */
> +	loongson_canfd_dma_sync_wr(priv);
> +	priv->rx_rd_pos = priv->rx_wr_pos;

[Severity: Medium]
This comment implies the engine can produce zero-fill while the
controller is disabled. If so, only the zeros written before this one
sync are skipped. Zeros written between here and chip_start() setting
REG_CONF_ENA would stay in the ring.

The first real frame would then land behind those zeros.
loongson_canfd_rx_is_empty_dma() or loongson_canfd_rx_one_frame_dma_bad_hdr()
would see a zero header and do:

	priv->rx_rd_pos = priv->rx_wr_pos;

Could that silently drop the first frames after an open or a restart?

Also, loongson_canfd_do_set_mode() calls loongson_canfd_reset() before
loongson_canfd_rxdma_stop(). That soft-resets the controller while its
DMA is still reading RX_DATA. Is that ordering intended?

[ ... ]

> @@ -995,15 +1245,29 @@ static void loongson_canfd_chip_stop(struct net_device *ndev)
>   */
>  static int loongson_canfd_do_set_mode(struct net_device *ndev, enum can_mode mode)
>  {
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
>  	int ret;
>
>  	switch (mode) {
>  	case CAN_MODE_START:
>  		loongson_canfd_reset(ndev);
>
> +		/*
> +		 * The FIFO starts from scratch; DMA and CPU read pointer
> +		 * must be realigned with the new FIFO start.
> +		 */
> +		loongson_canfd_rxdma_stop(priv);
> +
> +		ret = loongson_canfd_rxdma_start(ndev);

[Severity: Medium]
do_set_mode(CAN_MODE_START) is reached from can_restart(), through either
the restart work or a netlink restart. At that point NAPI is still
enabled and may be polling. What serializes this against
loongson_canfd_rx_napi()?

loongson_canfd_rxdma_start() resets rx_rd_pos, rx_wr_pos, rx_wr_off and
rx_cookie with plain stores. Meanwhile the poll may be inside
loongson_canfd_dma_sync_wr(), or rolling rx_rd_pos back to a saved_rd_pos
taken before the restart. Between terminate and resubmit, the old cookie
can also report residue 0, which loongson_canfd_dma_sync_wr() reads as a
wrap.

can_restart() assumes nothing is in flight during bus-off. Here, though,
err_state_change() neither stops the chip nor disables NAPI, and NAPI may
still be rescheduling itself. Could either interleaving leave rx_wr_pos
permanently offset from the real DMA position?

[ ... ]

> @@ -1435,6 +1698,247 @@ static int loongson_canfd_rx_deliver(struct net_device *ndev,
>  	}
>
>  	netif_receive_skb(skb);
> +	return 0;
> +}

[ ... ]

> +static bool loongson_canfd_rx_is_empty_dma(struct loongson_canfd_priv *priv)
> +{
> +	u32 rx_stat;
> +	u32 idx, h0, h1;
> +
> +	loongson_canfd_dma_sync_wr(priv);
> +
> +	if (priv->rx_wr_pos < priv->rx_rd_pos)
> +		goto discard;
> +
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &rx_stat);
> +
> +	/* A complete frame is waiting in the FIFO */
> +	if (FIELD_GET(REG_RX_STAT_RXFRC, rx_stat))
> +		return false;
> +
> +	/* FIFO holds a partial frame the DMA engine is still moving */
> +	if (!(rx_stat & REG_RX_STAT_RXE))
> +		return false;
> +
> +	/* Header pair not yet fully resident: let NAPI retry */
> +	if (priv->rx_wr_pos < priv->rx_rd_pos + 2)
> +		return false;

[Severity: High]
loongson_canfd_rx_is_empty_dma() runs before process_rx() on every
iteration of loongson_canfd_rx_napi(). Once the FIFO has drained
(RXFRC == 0, RXE set), any zero or implausible word pair at rx_rd_pos
makes it set rx_rd_pos = rx_wr_pos and return true. That discards
every unread word in the ring, up to the full 32 KiB, and no counter
is updated.

This seems to defeat the recovery paths in
loongson_canfd_rx_one_frame_dma().
loongson_canfd_rx_one_frame_dma_overrun() moves rx_rd_pos to
rx_wr_pos - RXDMA_NUM + 2 and returns 0, expecting the next iteration
to resync through loongson_canfd_rx_one_frame_dma_bad_hdr(). That
position is almost never a frame boundary.

If the FIFO is empty on the next iteration, the implausible pair is
seen here first and the whole ring is dropped. The forward scan in
loongson_canfd_rx_one_frame_dma_bad_hdr() never gets to run.

This function also never checks whether rx_wr_pos - rx_rd_pos exceeds
RXDMA_NUM. An overrun that is first noticed here is therefore dropped
silently, and rx_over_errors and rx_errors are never incremented.

Zero words sitting ahead of real frames behave the same way. This path
and both zero-header branches in
loongson_canfd_rx_one_frame_dma_bad_hdr() realign on rx_wr_pos. Any
valid frames the DMA has already written behind the zeros are lost as
well.

Should loongson_canfd_rx_is_empty_dma() only report emptiness and
leave resynchronization to process_rx()? And should these discards be
counted somewhere, for example in rx_dropped?

[Severity: High]
Doesn't this also return false in the normal drained state, where
wr == rd and the FIFO is idle? No path returns true for wr == rd.

If so, every NAPI poll ends by running LOONGSON_CANFD_DMA_WAIT_RETRY_MAX
(100) -EAGAIN iterations in loongson_canfd_rx_napi(). Each iteration does
a dmaengine_tx_status() call and an RX_STAT read.

The same check also makes NAPI reschedule forever on a quiet bus when the
tail of the ring can't be consumed:

- wr - rd == 1, for example after the -ENOMEM path below.

- A resync lands on a plausible header whose payload never arrives, so
  loongson_canfd_rx_one_frame_dma() keeps rolling back:

	if (priv->rx_wr_pos - priv->rx_rd_pos < d.rwcnt - 2) {
		priv->rx_rd_pos = saved_rd_pos;
		return -EAGAIN;
	}

In both cases loongson_canfd_rx_napi_done() sees wr > rd and calls
napi_schedule() with no limit. Is there a timeout or fallback meant to
stop this?

[ ... ]

> +static int loongson_canfd_rx_one_frame_dma_overrun(struct net_device *ndev,
> +						   u64 avail)
> +{
> +	struct loongson_canfd_priv *priv = netdev_priv(ndev);
> +	struct net_device_stats *stats = &ndev->stats;
> +	struct dma_tx_state dbg_state;
> +	u32 dbg_rx_stat = 0;
> +
> +	dmaengine_tx_status(priv->rx_ch, priv->rx_cookie, &dbg_state);
> +	regmap_read(priv->regmap, LOONGSON_CANFD_RX_STAT, &dbg_rx_stat);

[Severity: Low]
This isn't a bug, but dbg_state and dbg_rx_stat are read here and never
used, and the avail parameter is unused too. Can these be dropped?

The kernel-doc also says this resyncs "on the oldest still-valid word".
However, rx_wr_pos - RXDMA_NUM + 2 isn't a frame boundary, so the next
parse goes through the bad-header scan.

> +
> +	netdev_warn(ndev, "RX DMA overrun, resyncing\n");

[Severity: Low]
Neither this warning nor the "RX pointer inversion" warning in
loongson_canfd_rx_one_frame_dma() is rate limited. Both run from NAPI,
and bus flooding can trigger them. Should they use net_ratelimit(), as
the existing hardware overrun handler does?

[ ... ]

> +	/* Non-zero implausible header: scan forward for a plausible one */
> +	stats->rx_dropped++;

[Severity: Low]
This rx_dropped++ doesn't take stats_lock. Neither does the -ENOMEM path
in loongson_canfd_rx_one_frame_dma(), nor loongson_canfd_rx_note_bad().
On the other hand, loongson_canfd_rx_one_frame_dma_overrun() and the
pointer-inversion branch take stats_lock for the same counter.

All of these run in NAPI context. The unlocked updates only lose
increments if two polls overlap, which the rx_napi_done() window
described below makes possible. Could one scheme be used consistently?

> +
> +	for (tries = 0, pos = saved_pos + 1;
> +	     tries < 32 && pos + 1 < priv->rx_wr_pos;
> +	     tries++, pos++) {
> +		u32 h0 = loongson_canfd_ring_peek(priv, pos);
> +		u32 h1 = loongson_canfd_ring_peek(priv, pos + 1);
> +
> +		/* Zero-fill tail */
> +		if (h0 == 0 && h1 == 0) {
> +			loongson_canfd_dma_sync_wr(priv);
> +			priv->rx_rd_pos = priv->rx_wr_pos;
> +			priv->rx_bad_cnt = 0;
> +			return -ENODATA;
> +		}
> +
> +		if (loongson_canfd_hdr_plausible(h0, h1)) {
> +			priv->rx_rd_pos = pos;
> +			priv->rx_bad_cnt = 0;
> +			return 1;
> +		}
> +	}

[Severity: Medium]
The words scanned here are usually payload bytes sent by a remote node.
Once the reader has lost alignment, any word pair that passes
loongson_canfd_hdr_plausible() is accepted as a header. Alignment can be
lost through the -ENOMEM path's saved_rd_pos + 1, an overrun resync, or
the pointer races.

Could a remote node embed a plausible header in a frame's data? Once the
reader is desynchronized, the host would deliver a frame with an ID and
data that were never sent on the bus. That could get around ID-based
filtering done upstream, for example at a gateway.

[ ... ]

> +	ret = loongson_canfd_rx_deliver(ndev, &d);
> +	if (ret != -ENOMEM)
> +		return 1;
> +
> +	stats->rx_dropped++;
> +	priv->rx_rd_pos = saved_rd_pos + 1;
> +	if (++priv->rx_bad_cnt >= 64) {
> +		priv->rx_rd_pos = priv->rx_wr_pos;
> +		priv->rx_bad_cnt = 0;
> +		return -ENODATA;
> +	}
>  	return 1;
>  }

[Severity: High]
loongson_canfd_rx_deliver() returns -ENOMEM before reading any payload,
so rx_rd_pos is saved_rd_pos + 2 at this point. Setting it to
saved_rd_pos + 1 leaves the reader on hdr1 of a frame that was already
validated.

Should this skip the whole frame instead? The FIFO path does that with
loongson_canfd_rx_note_bad(ndev, d.rwcnt).

Take a 2-word frame (RTR, or classic with DLC=0) at the tail of the ring.
After this path rd = saved + 1 and wr = saved + 2:

loongson_canfd_rx_is_empty_dma()
	if (priv->rx_wr_pos < priv->rx_rd_pos + 2)
		return false;
loongson_canfd_rx_one_frame_dma()
	if (avail < 2)
		return -EAGAIN;
loongson_canfd_rx_napi()
	100 cpu_relax() retries, then completes
loongson_canfd_rx_napi_done()
	if (priv->rx_wr_pos > priv->rx_rd_pos) {
		napi_schedule(napi);

Can this keep one CPU spinning in softirq, with RBNEI still masked, until
another frame arrives?

For longer frames, (hdr1, payload0) gets parsed as the next header.
Depending on FIFO state, that either discards the whole ring in
loongson_canfd_rx_is_empty_dma() or sends the reader into the payload
scan in loongson_canfd_rx_one_frame_dma_bad_hdr().

Also, rx_bad_cnt is reset to 0 on the valid-header path just above. Can
the ++priv->rx_bad_cnt >= 64 bail-out ever fire?

[ ... ]

> @@ -1532,20 +2036,52 @@ static bool loongson_canfd_rx_overrun_event(struct net_device *ndev)

[ ... ]

>  	if (!can_complete || !napi_complete_done(napi, work_done))
>  		return;
>
> -	regmap_write(priv->regmap, LOONGSON_CANFD_INT_MASK,
> -		     FIELD_PREP(REG_INT_MASK_CLR, REG_INT_STAT_RBNEI));
> +	/*
> +	 * Catch a DMADI that was swallowed while SCHED was still set,
> +	 * and a stale partial header that rx_is_empty_dma() skipped.
> +	 */
> +	if (priv->rx_ch) {
> +		loongson_canfd_dma_sync_wr(priv);
> +		if (priv->rx_wr_pos > priv->rx_rd_pos) {
> +			napi_schedule(napi);
> +			return;
> +		}
> +	}

[Severity: High]
NAPI ownership has already been released by napi_complete_done() at this
point. DMADI is never masked and fires for every message. Can an
interrupt on another CPU, or preemption on PREEMPT_RT, start a second
poll in this window? That poll would run
loongson_canfd_rx_is_empty_dma() -> loongson_canfd_dma_sync_wr() at the
same time as this code.

loongson_canfd_dma_sync_wr() does an unlocked read-modify-write:

	if (off < priv->rx_wr_off)
		priv->rx_wr_pos += LOONGSON_CANFD_RXDMA_NUM -
				   priv->rx_wr_off + off;
	else
		priv->rx_wr_pos += off - priv->rx_wr_off;

	priv->rx_wr_off = off;

If both contexts compute the delta from the same old rx_wr_off, the delta
gets added twice. rx_wr_pos would then stay ahead of the real DMA
position. Would stale ring contents from the previous lap be parsed as
new frames until the next restart?

> +
> +	/*
> +	 * Flush an overrun latched while DOI was masked (e.g. during
> +	 * NAPI). DOR is sticky and the handler is idempotent, so this
> +	 * costs one regmap_read in the common case.
> +	 */
> +	loongson_canfd_rx_overrun_event(ndev);

[Severity: Medium]
The order of these checks seems to leave a window. The ring is sampled
first through loongson_canfd_dma_sync_wr(). Then
loongson_canfd_rx_overrun_event() and the RX_STAT read run, and only
after that is RBNEI unmasked.

Suppose a frame is still in the hardware FIFO when
loongson_canfd_dma_sync_wr() runs, so rx_wr_pos == rx_rd_pos. The DMA
engine keeps running and can move that frame into the ring before
RX_STAT is read. RX_STAT then shows RXFRC == 0 with RXE set, so RBNEI
is unmasked and the function returns.

The FIFO is already empty by then, so no empty -> non-empty edge will
follow. rx_wr_pos is not sampled again after the unmask.

A frame of 8 to 72 bytes doesn't cross a 4 KiB period boundary. If
DMADI only fires once per period, as the new chip_start() comment
says, nothing reschedules NAPI. Wouldn't that frame sit unread in the
ring until the next frame arrives on the bus?

Would it work to read RX_STAT and unmask RBNEI first, and only then
re-sync rx_wr_pos and reschedule if rx_wr_pos > rx_rd_pos? Or does
DMADI really fire for every message, which would cover this case?

[Severity: Low]
loongson_canfd_rx_overrun_event() now also runs here in softirq, as well
as from the hardirq DOI handler. The ISR masks DOI around its own call,
but that doesn't exclude this caller.

If both read the sticky DOR bit before either one writes REG_CMD_CDO,
would a single overrun be counted twice in rx_over_errors and rx_errors,
and produce two CAN_ERR_CRTL_RX_OVERFLOW frames? The handler is only
idempotent for sequential calls.

[ ... ]

> @@ -1847,6 +2410,10 @@ static void loongson_canfd_err_interrupt(struct net_device *ndev, u32 isr)
>   * optional HW TX timestamp) and counts it sent; TX_BS_FAIL and
>   * TX_BS_CANCEL free the skb as dropped. Clears the completion record
>   * per slot and wakes the queue if a buffer became free.
> + *
> + * RBNEI is serviced by masking it, resuming the paused RX DMA channel
> + * (no-op in poll mode) and scheduling NAPI. DMADI is never enabled, so
> + * no DMA completion IRQ is expected in the RX path.
>   */

[Severity: Low]
This isn't a bug, but this comment doesn't match the code.
loongson_canfd_tx_interrupt() doesn't touch RBNEI or the DMA channel.
chip_start() enables DMADI, the ISR handles DMADI, and there is no
pause/resume.

A few other comments also disagree with the code or with each other:

- loongson_canfd_rxdma_start() and loongson_canfd_rx_is_empty_dma() say
  the engine stalls on an empty RX_DATA. Yet there are several "DMA
  zero-fill" recovery paths, and rxdma_start() skips zero-fill.

- The kernel-doc of loongson_canfd_rx_overrun_event() and its "Hard IRQ:
  use netif_rx()" comment still describe a hard-IRQ-only caller.
  loongson_canfd_rx_napi_done() now also calls it from NAPI.

[ ... ]

> @@ -1977,6 +2549,13 @@ static irqreturn_t loongson_canfd_interrupt(int irq, void *dev_id)
>  			napi_schedule(&priv->napi);
>  		}
>
> +		if (isr & REG_INT_STAT_DMADI) {
> +			/* W1C: fresh data in the ring, schedule NAPI */
> +			regmap_write(priv->regmap, LOONGSON_CANFD_INT_STAT,
> +				     REG_INT_STAT_DMADI);
> +			napi_schedule(&priv->napi);
> +		}

[Severity: Medium]
REG_INT_STAT_DMADI is described as the "Message DMA Transfer Complete
Interrupt", so it looks like a per-message event. Unlike RBNEI, it is
never masked while NAPI is running.

Under high traffic, does this raise a hardware IRQ for every received
frame even while NAPI is polling? That would work against NAPI interrupt
mitigation.

The comment added to loongson_canfd_chip_start() says DMADI fires "once
per cyclic period", which doesn't seem to match.

[ ... ]

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/cover.1794213427.git.zhoubinbin%40loongson.cn

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-11-10 22:20 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-09  8:42 [PATCH v9 0/2] Add Loongson CAN-FD controller driver Binbin Zhou
2026-11-09  8:42 ` [PATCH v9 1/2] can: " Binbin Zhou
2026-11-09  9:31   ` sashiko-bot
2026-11-10 22:20   ` netdev-bot+sashiko
2026-11-09  8:42 ` [PATCH v9 2/2] can: loongson_canfd: Add RXDMA support Binbin Zhou
2026-11-09  9:51   ` sashiko-bot
2026-11-10 22:20   ` netdev-bot+sashiko

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox