* [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
* 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 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
* [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 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 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