From e0d3974dc51951879604eaa3dd61d76ce93379c2 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Fri, 26 Dec 2025 22:40:46 +0300 Subject: [PATCH 1/7] Init CAN driver Holtek ht42b416 for WB8 --- .../bindings/net/can/holtek,ht42b416.yaml | 55 ++ MAINTAINERS | 7 + .../allwinner/sun50i-h616-wirenboard85x.dtsi | 7 +- arch/arm64/configs/wb8.config | 1 + drivers/net/can/Kconfig | 11 + drivers/net/can/Makefile | 1 + drivers/net/can/ht42b416.c | 815 ++++++++++++++++++ 7 files changed, 896 insertions(+), 1 deletion(-) create mode 100644 Documentation/devicetree/bindings/net/can/holtek,ht42b416.yaml create mode 100644 drivers/net/can/ht42b416.c diff --git a/Documentation/devicetree/bindings/net/can/holtek,ht42b416.yaml b/Documentation/devicetree/bindings/net/can/holtek,ht42b416.yaml new file mode 100644 index 0000000000000..f1c759352a977 --- /dev/null +++ b/Documentation/devicetree/bindings/net/can/holtek,ht42b416.yaml @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) +%YAML 1.2 +--- +$id: http://devicetree.org/schemas/net/can/holtek,ht42b416.yaml# +$schema: http://devicetree.org/meta-schemas/core.yaml# + +title: Holtek HT42B416 UART to CAN bus bridge + +maintainers: + - Anton Tarasov + +description: + The Holtek HT42B416-x devices expose a CAN 2.0A/B controller via a UART + interface. The controller is configured with ASCII commands terminated by + carriage return characters and is normally attached to a SoC UART using the + serdev framework. + +allOf: + - $ref: /schemas/net/can/can-controller.yaml# + +properties: + compatible: + const: holtek,ht42b416 + + current-speed: + description: UART baud rate used to communicate with the bridge. + default: 115200 + + enable-gpios: + maxItems: 1 + description: + Optional GPIO used to drive the CE pin of the bridge. The line should be + driven high to enable the device and low to reset or power it down. + +required: + - compatible + +unevaluatedProperties: false + +examples: + - | + #include + + &uart4 { + pinctrl-names = "default"; + pinctrl-0 = <&pinctrl_mod3_txrx_uart>; + status = "okay"; + + can@0 { + compatible = "holtek,ht42b416"; + current-speed = <115200>; + enable-gpios = <&pio PG 10 GPIO_ACTIVE_HIGH>; + }; + }; +... diff --git a/MAINTAINERS b/MAINTAINERS index 1aabf1c15bb30..48bb0df4c44d5 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -9413,6 +9413,13 @@ M: Alexander Shishkin S: Maintained F: drivers/hwtracing/ +HOLTEK HT42B416 CAN DRIVER +M: Anton Tarasov +L: linux-can@vger.kernel.org +S: Maintained +F: Documentation/devicetree/bindings/net/can/holtek,ht42b416.yaml +F: drivers/net/can/ht42b416.c + HARMONY SOUND DRIVER L: linux-parisc@vger.kernel.org S: Maintained diff --git a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi index 51983e30bd513..9922bbbe9cf4c 100644 --- a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi +++ b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi @@ -1065,7 +1065,12 @@ pinctrl-0 = <&pinctrl_mod3_txrx_uart>; cts-override; - status = "disabled"; + status = "okay"; + + can_mod3: can@0 { + compatible = "holtek,ht42b416"; + current-speed = <115200>; + }; }; &uart5 { /* MOD4 */ diff --git a/arch/arm64/configs/wb8.config b/arch/arm64/configs/wb8.config index 91c34be0a840a..6e6c4b1491433 100644 --- a/arch/arm64/configs/wb8.config +++ b/arch/arm64/configs/wb8.config @@ -161,6 +161,7 @@ CONFIG_INET6_XFRM_MODE_BEET=m # CAN support CONFIG_CAN=y CONFIG_CAN_GS_USB=m +CONFIG_CAN_HT42B416=m # Docker CONFIG_BLK_CGROUP=y diff --git a/drivers/net/can/Kconfig b/drivers/net/can/Kconfig index eb410714afc20..113f2a100ff79 100644 --- a/drivers/net/can/Kconfig +++ b/drivers/net/can/Kconfig @@ -124,6 +124,17 @@ config CAN_CAN327 If this driver is built as a module, it will be called can327. +config CAN_HT42B416 + tristate "Holtek HT42B416 UART-to-CAN bridge" + depends on SERIAL_DEV_BUS && OF + help + Enable support for the Holtek HT42B416 UART to CAN bridge chips. + The driver uses the serdev framework to talk to the bridge via a SoC + UART and exposes a SocketCAN network interface. + + To compile this driver as a module, choose M here: the module will be + called ht42b416. + config CAN_FLEXCAN tristate "Support for Freescale FLEXCAN based chips" depends on OF || COLDFIRE || COMPILE_TEST diff --git a/drivers/net/can/Makefile b/drivers/net/can/Makefile index ff8f76295d13b..bb04f4e9e9b9e 100644 --- a/drivers/net/can/Makefile +++ b/drivers/net/can/Makefile @@ -16,6 +16,7 @@ obj-y += softing/ obj-$(CONFIG_CAN_AT91) += at91_can.o obj-$(CONFIG_CAN_BXCAN) += bxcan.o obj-$(CONFIG_CAN_CAN327) += can327.o +obj-$(CONFIG_CAN_HT42B416) += ht42b416.o obj-$(CONFIG_CAN_CC770) += cc770/ obj-$(CONFIG_CAN_C_CAN) += c_can/ obj-$(CONFIG_CAN_CTUCANFD) += ctucanfd/ diff --git a/drivers/net/can/ht42b416.c b/drivers/net/can/ht42b416.c new file mode 100644 index 0000000000000..c8110a18aed96 --- /dev/null +++ b/drivers/net/can/ht42b416.c @@ -0,0 +1,815 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Holtek HT42B416 UART-to-CAN bridge driver + * + * The controller is configured with ASCII commands terminated by carriage + * return characters. Payload bytes are transferred in binary form. + * + * Copyright (C) 2024 Wiren Board + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define HT42B416_MAX_DATA_LEN 8 +#define HT42B416_MAX_CMD_LEN 16 +#define HT42B416_MAX_FRAME_LEN (1 + 4 + 1 + HT42B416_MAX_DATA_LEN + 1) +#define HT42B416_RX_BUF_LEN 64 + +#define HT42B416_CMD_TIMEOUT_MS 250 +#define HT42B416_POWERUP_DELAY_US 200 +#define HT42B416_TX_TIMEOUT (2 * HZ) + +enum ht42b416_wait_ack { + HT42B416_WAIT_NONE, + HT42B416_WAIT_CR, +}; + +enum ht42b416_tx_expect { + HT42B416_TX_NONE, + HT42B416_TX_EXPECT_Z, + HT42B416_TX_EXPECT_CAP_Z, +}; + +enum ht42b416_status { + HT42B416_STATUS_TX_OK = 6, + HT42B416_STATUS_BUS_OFF = -8, + HT42B416_STATUS_ERR_CRC = -10, + HT42B416_STATUS_ERR_BIT1 = -11, + HT42B416_STATUS_ERR_BIT0 = -12, + HT42B416_STATUS_ERR_ACK = -13, + HT42B416_STATUS_ERR_FORM = -14, + HT42B416_STATUS_ERR_STUFF = -15, + HT42B416_STATUS_ERR_UNKNOWN = -16, +}; + +struct ht42b416_priv { + struct can_priv can; + struct serdev_device *serdev; + struct gpio_desc *enable_gpiod; + + struct work_struct tx_work; + spinlock_t tx_lock; /* protects below fields */ + u8 tx_buf[HT42B416_MAX_FRAME_LEN]; + size_t tx_len; + size_t tx_pos; + enum ht42b416_tx_expect tx_expect; + u8 tx_dlc; + bool tx_busy; + + u8 rx_buf[HT42B416_RX_BUF_LEN]; + size_t rx_len; + + struct completion ack_complete; + enum ht42b416_wait_ack wait_ack; + struct mutex cmd_lock; /* serialises configuration commands */ + + u32 uart_baud; + bool running; +}; + +static const u32 ht42b416_bitrate_const[] = { + 5000, 10000, 20000, 50000, 100000, + 125000, 250000, 500000, 800000, 1000000, +}; + +static const u8 ht42b416_filter_all_std[] = { 'm', 0x00, 0x00 }; +static const u8 ht42b416_code_all_std[] = { 'M', 0x00, 0x00 }; +static const u8 ht42b416_filter_all_ext[] = { 'm', 0x00, 0x00, 0x00, 0x00 }; +static const u8 ht42b416_code_all_ext[] = { 'M', 0x00, 0x00, 0x00, 0x00 }; + +static int ht42b416_write_raw(struct ht42b416_priv *priv, + const u8 *buf, size_t len) +{ + int ret; + + ret = serdev_device_write(priv->serdev, buf, len, + msecs_to_jiffies(HT42B416_CMD_TIMEOUT_MS)); + if (ret < 0) + return ret; + if (ret != len) + return -ETIMEDOUT; + + return 0; +} + +static int ht42b416_send_cmd_locked(struct ht42b416_priv *priv, + const u8 *payload, size_t len, + enum ht42b416_wait_ack wait) +{ + u8 buffer[HT42B416_MAX_CMD_LEN]; + int ret; + + if (!len || len >= HT42B416_MAX_CMD_LEN) + return -EINVAL; + + memcpy(buffer, payload, len); + buffer[len++] = '\r'; + + if (wait != HT42B416_WAIT_NONE) { + reinit_completion(&priv->ack_complete); + WRITE_ONCE(priv->wait_ack, wait); + } + + ret = ht42b416_write_raw(priv, buffer, len); + if (ret || wait == HT42B416_WAIT_NONE) + goto out; + + if (!wait_for_completion_timeout(&priv->ack_complete, + msecs_to_jiffies(HT42B416_CMD_TIMEOUT_MS))) + ret = -ETIMEDOUT; + +out: + if (ret && wait != HT42B416_WAIT_NONE) + WRITE_ONCE(priv->wait_ack, HT42B416_WAIT_NONE); + + return ret; +} + +static int ht42b416_send_cmd(struct ht42b416_priv *priv, + const u8 *payload, size_t len, + enum ht42b416_wait_ack wait) +{ + int ret; + + mutex_lock(&priv->cmd_lock); + ret = ht42b416_send_cmd_locked(priv, payload, len, wait); + mutex_unlock(&priv->cmd_lock); + + return ret; +} + +static int ht42b416_bitrate_to_code(u32 bitrate) +{ + int i; + + for (i = 0; i < ARRAY_SIZE(ht42b416_bitrate_const); i++) + if (ht42b416_bitrate_const[i] == bitrate) + return i; + + return -EINVAL; +} + +static void ht42b416_abort_tx(struct ht42b416_priv *priv) +{ + struct net_device *ndev = priv->can.dev; + unsigned long flags; + + spin_lock_irqsave(&priv->tx_lock, flags); + priv->tx_len = 0; + priv->tx_pos = 0; + priv->tx_expect = HT42B416_TX_NONE; + priv->tx_busy = false; + spin_unlock_irqrestore(&priv->tx_lock, flags); + + can_free_echo_skb(ndev, 0, NULL); + netif_wake_queue(ndev); +} + +static const struct ethtool_ops ht42b416_ethtool_ops = { + .get_ts_info = ethtool_op_get_ts_info, +}; + +static int ht42b416_hw_stop(struct ht42b416_priv *priv) +{ + static const u8 close_cmd[] = { 'C' }; + + if (!priv->running) + goto disable_gpio; + + netif_stop_queue(priv->can.dev); + ht42b416_abort_tx(priv); + + ht42b416_send_cmd(priv, close_cmd, sizeof(close_cmd), + HT42B416_WAIT_CR); + priv->running = false; + +disable_gpio: + if (priv->enable_gpiod) + gpiod_set_value_cansleep(priv->enable_gpiod, 0); + + priv->can.state = CAN_STATE_STOPPED; + + return 0; +} + +static int ht42b416_hw_start(struct ht42b416_priv *priv) +{ + struct net_device *ndev = priv->can.dev; + u8 cmd[5]; + int ret, code; + u8 open_cmd; + + if (priv->running) + return 0; + + if (priv->enable_gpiod) { + gpiod_set_value_cansleep(priv->enable_gpiod, 1); + usleep_range(HT42B416_POWERUP_DELAY_US, + HT42B416_POWERUP_DELAY_US + 200); + } + + priv->rx_len = 0; + + /* Close device to ensure clean state */ + cmd[0] = 'C'; + ret = ht42b416_send_cmd(priv, cmd, 1, HT42B416_WAIT_CR); + if (ret) + return ret; + + code = ht42b416_bitrate_to_code(priv->can.bittiming.bitrate); + if (code < 0) + return code; + + cmd[0] = 'S'; + cmd[1] = code; + ret = ht42b416_send_cmd(priv, cmd, 2, HT42B416_WAIT_CR); + if (ret) + return ret; + + ret = ht42b416_send_cmd(priv, ht42b416_filter_all_std, + sizeof(ht42b416_filter_all_std), + HT42B416_WAIT_CR); + if (ret) + return ret; + + ret = ht42b416_send_cmd(priv, ht42b416_code_all_std, + sizeof(ht42b416_code_all_std), + HT42B416_WAIT_CR); + if (ret) + return ret; + + ret = ht42b416_send_cmd(priv, ht42b416_filter_all_ext, + sizeof(ht42b416_filter_all_ext), + HT42B416_WAIT_CR); + if (ret) + return ret; + + ret = ht42b416_send_cmd(priv, ht42b416_code_all_ext, + sizeof(ht42b416_code_all_ext), + HT42B416_WAIT_CR); + if (ret) + return ret; + + if (priv->can.ctrlmode & CAN_CTRLMODE_LOOPBACK) + open_cmd = 'l'; + else if (priv->can.ctrlmode & CAN_CTRLMODE_LISTENONLY) + open_cmd = 'L'; + else + open_cmd = 'O'; + + ret = ht42b416_send_cmd(priv, &open_cmd, 1, HT42B416_WAIT_CR); + if (ret) + return ret; + + priv->tx_pos = 0; + priv->tx_len = 0; + priv->tx_expect = HT42B416_TX_NONE; + priv->tx_busy = false; + priv->running = true; + priv->can.state = CAN_STATE_ERROR_ACTIVE; + netif_wake_queue(ndev); + + return 0; +} + +static int ht42b416_tx_kick_locked(struct ht42b416_priv *priv) +{ + while (priv->tx_pos < priv->tx_len) { + int written; + + written = serdev_device_write_buf(priv->serdev, + priv->tx_buf + priv->tx_pos, + priv->tx_len - priv->tx_pos); + if (written < 0) { + netdev_err(priv->can.dev, + "unable to push UART data: %d\n", written); + priv->tx_pos = priv->tx_len; + return written; + } + + if (!written) + break; + + priv->tx_pos += written; + } + + return 0; +} + +static void ht42b416_tx_work(struct work_struct *work) +{ + struct ht42b416_priv *priv = + container_of(work, struct ht42b416_priv, tx_work); + unsigned long flags; + + spin_lock_irqsave(&priv->tx_lock, flags); + if (priv->tx_pos < priv->tx_len) + ht42b416_tx_kick_locked(priv); + spin_unlock_irqrestore(&priv->tx_lock, flags); +} + +static void ht42b416_tx_wakeup(struct serdev_device *serdev) +{ + struct ht42b416_priv *priv = serdev_device_get_drvdata(serdev); + + serdev_device_write_wakeup(serdev); + schedule_work(&priv->tx_work); +} + +static void ht42b416_finish_tx(struct ht42b416_priv *priv, u8 ack_byte) +{ + struct net_device *ndev = priv->can.dev; + unsigned long flags; + unsigned int tx_bytes; + + spin_lock_irqsave(&priv->tx_lock, flags); + if (!priv->tx_busy || priv->tx_pos < priv->tx_len) { + spin_unlock_irqrestore(&priv->tx_lock, flags); + return; + } + + if ((ack_byte != 'z' || priv->tx_expect != HT42B416_TX_EXPECT_Z) && + (ack_byte != 'Z' || priv->tx_expect != HT42B416_TX_EXPECT_CAP_Z)) + netdev_warn(ndev, "unexpected TX ack 0x%02x\n", ack_byte); + + priv->tx_busy = false; + priv->tx_expect = HT42B416_TX_NONE; + priv->tx_len = 0; + priv->tx_pos = 0; + spin_unlock_irqrestore(&priv->tx_lock, flags); + + tx_bytes = can_get_echo_skb(ndev, 0, NULL); + ndev->stats.tx_packets++; + ndev->stats.tx_bytes += tx_bytes; + + netif_wake_queue(ndev); +} + +static void ht42b416_handle_status(struct ht42b416_priv *priv, s8 status) +{ + struct net_device *ndev = priv->can.dev; + struct sk_buff *skb; + struct can_frame *cf; + + switch (status) { + case HT42B416_STATUS_TX_OK: + case HT42B416_STATUS_ERR_UNKNOWN: + return; + case HT42B416_STATUS_BUS_OFF: + if (priv->can.state != CAN_STATE_BUS_OFF) { + priv->can.can_stats.bus_off++; + priv->running = false; + can_bus_off(ndev); + } + return; + default: + break; + } + + skb = alloc_can_err_skb(ndev, &cf); + if (!skb) { + ndev->stats.rx_dropped++; + return; + } + + memset(cf->data, 0, CAN_ERR_DLC); + cf->can_id |= CAN_ERR_PROT | CAN_ERR_BUSERROR; + + switch (status) { + case HT42B416_STATUS_ERR_CRC: + cf->data[2] |= CAN_ERR_PROT_BIT; + cf->data[3] = CAN_ERR_PROT_LOC_CRC_SEQ; + break; + case HT42B416_STATUS_ERR_BIT1: + cf->data[2] |= CAN_ERR_PROT_BIT1; + break; + case HT42B416_STATUS_ERR_BIT0: + cf->data[2] |= CAN_ERR_PROT_BIT0; + break; + case HT42B416_STATUS_ERR_ACK: + cf->can_id |= CAN_ERR_ACK; + cf->data[3] = CAN_ERR_PROT_LOC_ACK; + break; + case HT42B416_STATUS_ERR_FORM: + cf->data[2] |= CAN_ERR_PROT_FORM; + break; + case HT42B416_STATUS_ERR_STUFF: + cf->data[2] |= CAN_ERR_PROT_STUFF; + break; + default: + break; + } + + priv->can.can_stats.bus_error++; + ndev->stats.rx_packets++; + ndev->stats.rx_bytes += cf->len; + netif_rx(skb); +} + +static void ht42b416_signal_ack(struct ht42b416_priv *priv) +{ + enum ht42b416_wait_ack wait; + + wait = READ_ONCE(priv->wait_ack); + if (wait == HT42B416_WAIT_CR) { + WRITE_ONCE(priv->wait_ack, HT42B416_WAIT_NONE); + complete(&priv->ack_complete); + } +} + +static int ht42b416_parse_frame(struct ht42b416_priv *priv, + const u8 *buf, size_t len) +{ + struct net_device *ndev = priv->can.dev; + struct sk_buff *skb; + struct can_frame *cf; + bool is_eff, is_rtr; + u8 dlc; + size_t min_len, idx; + u32 can_id; + + if (!len) + return -EINVAL; + + is_eff = buf[0] == 'T' || buf[0] == 'R'; + is_rtr = buf[0] == 'r' || buf[0] == 'R'; + + if (is_eff) { + min_len = 1 + 4 + 1; + if (len < min_len) + return -EINVAL; + can_id = ((u32)buf[1] << 24) | + ((u32)buf[2] << 16) | + ((u32)buf[3] << 8) | + buf[4]; + can_id &= CAN_EFF_MASK; + dlc = buf[5]; + idx = 6; + } else { + min_len = 1 + 2 + 1; + if (len < min_len) + return -EINVAL; + can_id = ((u32)buf[1] << 8) | buf[2]; + can_id &= CAN_SFF_MASK; + dlc = buf[3]; + idx = 4; + } + + if (dlc > CAN_MAX_DLEN) + return -EINVAL; + + if (!is_rtr && len != idx + dlc) + return -EINVAL; + + skb = alloc_can_skb(ndev, &cf); + if (!skb) { + ndev->stats.rx_dropped++; + return 0; + } + + cf->can_id = can_id; + if (is_eff) + cf->can_id |= CAN_EFF_FLAG; + if (is_rtr) + cf->can_id |= CAN_RTR_FLAG; + + cf->len = dlc; + if (!is_rtr && dlc) + memcpy(cf->data, &buf[idx], dlc); + + ndev->stats.rx_packets++; + ndev->stats.rx_bytes += cf->len; + netif_rx(skb); + + return 0; +} + +static void ht42b416_process_msg(struct ht42b416_priv *priv, + const u8 *buf, size_t len) +{ + struct net_device *ndev = priv->can.dev; + + if (!len) { + ht42b416_signal_ack(priv); + return; + } + + switch (buf[0]) { + case 't': + case 'T': + case 'r': + case 'R': + if (ht42b416_parse_frame(priv, buf, len) && net_ratelimit()) + netdev_warn(ndev, "invalid frame len=%zu\n", len); + break; + case 'z': + case 'Z': + ht42b416_finish_tx(priv, buf[0]); + break; + case 'F': + if (len >= 2) + ht42b416_handle_status(priv, (s8)buf[1]); + break; + default: + netdev_dbg(ndev, "dropping response 0x%02x len=%zu\n", + buf[0], len); + break; + } +} + +static size_t ht42b416_receive(struct serdev_device *serdev, + const u8 *data, size_t count) +{ + struct ht42b416_priv *priv = serdev_device_get_drvdata(serdev); + size_t i; + + for (i = 0; i < count; i++) { + u8 byte = data[i]; + + if (byte == '\r' || byte == '\n') { + if (priv->rx_len < HT42B416_RX_BUF_LEN) + ht42b416_process_msg(priv, + priv->rx_buf, + priv->rx_len); + else + netdev_warn(priv->can.dev, + "RX buffer overflow (%zu bytes)\n", + priv->rx_len); + priv->rx_len = 0; + continue; + } + + if (priv->rx_len >= HT42B416_RX_BUF_LEN) { + priv->rx_len = HT42B416_RX_BUF_LEN; + continue; + } + + priv->rx_buf[priv->rx_len++] = byte; + } + + return count; +} + +static const struct serdev_device_ops ht42b416_serdev_ops = { + .receive_buf = ht42b416_receive, + .write_wakeup = ht42b416_tx_wakeup, +}; + +static netdev_tx_t ht42b416_start_xmit(struct sk_buff *skb, + struct net_device *ndev) +{ + struct ht42b416_priv *priv = netdev_priv(ndev); + struct can_frame *cf = (struct can_frame *)skb->data; + unsigned long flags; + bool eff, rtr; + u8 *pos; + int err; + + if (can_dev_dropped_skb(ndev, skb)) + return NETDEV_TX_OK; + + eff = cf->can_id & CAN_EFF_FLAG; + rtr = cf->can_id & CAN_RTR_FLAG; + + spin_lock_irqsave(&priv->tx_lock, flags); + if (priv->tx_busy) { + netif_stop_queue(ndev); + spin_unlock_irqrestore(&priv->tx_lock, flags); + return NETDEV_TX_BUSY; + } + + pos = priv->tx_buf; + if (eff) { + u32 id = cf->can_id & CAN_EFF_MASK; + + *pos++ = rtr ? 'R' : 'T'; + *pos++ = id >> 24; + *pos++ = id >> 16; + *pos++ = id >> 8; + *pos++ = id; + } else { + u16 id = cf->can_id & CAN_SFF_MASK; + + *pos++ = rtr ? 'r' : 't'; + *pos++ = id >> 8; + *pos++ = id; + } + + *pos++ = cf->len; + + if (!rtr && cf->len) { + memcpy(pos, cf->data, cf->len); + pos += cf->len; + } + + *pos++ = '\r'; + + priv->tx_len = pos - priv->tx_buf; + priv->tx_pos = 0; + priv->tx_busy = true; + priv->tx_expect = eff ? HT42B416_TX_EXPECT_CAP_Z : HT42B416_TX_EXPECT_Z; + priv->tx_dlc = cf->len; + + netif_stop_queue(ndev); + can_put_echo_skb(skb, ndev, 0, 0); + + err = ht42b416_tx_kick_locked(priv); + if (err) { + priv->tx_busy = false; + priv->tx_len = 0; + priv->tx_pos = 0; + priv->tx_expect = HT42B416_TX_NONE; + spin_unlock_irqrestore(&priv->tx_lock, flags); + ndev->stats.tx_errors++; + can_free_echo_skb(ndev, 0, NULL); + netif_wake_queue(ndev); + return NETDEV_TX_OK; + } + + if (priv->tx_pos < priv->tx_len) + schedule_work(&priv->tx_work); + spin_unlock_irqrestore(&priv->tx_lock, flags); + + return NETDEV_TX_OK; +} + +static int ht42b416_open(struct net_device *ndev) +{ + struct ht42b416_priv *priv = netdev_priv(ndev); + int ret; + + ret = open_candev(ndev); + if (ret) + return ret; + + ret = ht42b416_hw_start(priv); + if (ret) { + close_candev(ndev); + return ret; + } + + netif_start_queue(ndev); + return 0; +} + +static int ht42b416_close(struct net_device *ndev) +{ + struct ht42b416_priv *priv = netdev_priv(ndev); + + netif_stop_queue(ndev); + ht42b416_hw_stop(priv); + close_candev(ndev); + + return 0; +} + +static void ht42b416_tx_timeout(struct net_device *ndev, + unsigned int txqueue) +{ + struct ht42b416_priv *priv = netdev_priv(ndev); + + netdev_warn(ndev, "TX timeout\n"); + ndev->stats.tx_errors++; + ht42b416_abort_tx(priv); +} + +static const struct net_device_ops ht42b416_netdev_ops = { + .ndo_open = ht42b416_open, + .ndo_stop = ht42b416_close, + .ndo_start_xmit = ht42b416_start_xmit, + .ndo_tx_timeout = ht42b416_tx_timeout, +}; + +static int ht42b416_set_mode(struct net_device *ndev, enum can_mode mode) +{ + struct ht42b416_priv *priv = netdev_priv(ndev); + + switch (mode) { + case CAN_MODE_START: + return ht42b416_hw_start(priv); + case CAN_MODE_STOP: + return ht42b416_hw_stop(priv); + default: + return -EOPNOTSUPP; + } +} + +static int ht42b416_probe(struct serdev_device *serdev) +{ + struct device *dev = &serdev->dev; + struct net_device *ndev; + struct ht42b416_priv *priv; + u32 baud = 115200; + int ret; + + ndev = alloc_candev(sizeof(*priv), 1); + if (!ndev) + return -ENOMEM; + + ndev->flags |= IFF_ECHO; /* we support local echo */ + + priv = netdev_priv(ndev); + priv->serdev = serdev; + priv->uart_baud = baud; + priv->running = false; + priv->rx_len = 0; + mutex_init(&priv->cmd_lock); + init_completion(&priv->ack_complete); + spin_lock_init(&priv->tx_lock); + INIT_WORK(&priv->tx_work, ht42b416_tx_work); + priv->wait_ack = HT42B416_WAIT_NONE; + + priv->enable_gpiod = devm_gpiod_get_optional(dev, "enable", GPIOD_OUT_LOW); + if (IS_ERR(priv->enable_gpiod)) { + ret = PTR_ERR(priv->enable_gpiod); + goto err_free; + } + + device_property_read_u32(dev, "current-speed", &priv->uart_baud); + + serdev_device_set_drvdata(serdev, priv); + serdev_device_set_client_ops(serdev, &ht42b416_serdev_ops); + + ret = devm_serdev_device_open(dev, serdev); + if (ret) + goto err_free; + + serdev_device_set_baudrate(serdev, priv->uart_baud); + serdev_device_set_flow_control(serdev, false); + serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); + + priv->can.clock.freq = 0; + priv->can.ctrlmode_supported = CAN_CTRLMODE_LOOPBACK | + CAN_CTRLMODE_LISTENONLY; + priv->can.do_set_mode = ht42b416_set_mode; + priv->can.bitrate_const = ht42b416_bitrate_const; + priv->can.bitrate_const_cnt = ARRAY_SIZE(ht42b416_bitrate_const); + + ndev->netdev_ops = &ht42b416_netdev_ops; + ndev->ethtool_ops = &ht42b416_ethtool_ops; + ndev->watchdog_timeo = HT42B416_TX_TIMEOUT; + + SET_NETDEV_DEV(ndev, dev); + + ret = register_candev(ndev); + if (ret) + goto err_free; + + dev_info(dev, "Holtek HT42B416 bridge at %u baud\n", priv->uart_baud); + + return 0; + +err_free: + free_candev(ndev); + return ret; +} + +static void ht42b416_remove(struct serdev_device *serdev) +{ + struct ht42b416_priv *priv = serdev_device_get_drvdata(serdev); + struct net_device *ndev = priv->can.dev; + + cancel_work_sync(&priv->tx_work); + ht42b416_hw_stop(priv); + unregister_candev(ndev); + free_candev(ndev); +} + +static const struct of_device_id ht42b416_of_match[] = { + { .compatible = "holtek,ht42b416" }, + { } +}; +MODULE_DEVICE_TABLE(of, ht42b416_of_match); + +static struct serdev_device_driver ht42b416_driver = { + .driver = { + .name = "ht42b416", + .of_match_table = ht42b416_of_match, + }, + .probe = ht42b416_probe, + .remove = ht42b416_remove, +}; + +module_serdev_device_driver(ht42b416_driver); + +MODULE_AUTHOR("Anton Tarasov "); +MODULE_DESCRIPTION("Holtek HT42B416 UART-to-CAN bridge driver"); +MODULE_LICENSE("GPL"); From c0b4a924b163a85eecbc0395843da9fdc03364eb Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Sat, 27 Dec 2025 00:58:16 +0300 Subject: [PATCH 2/7] Change slot for CAN: MOD3 to MOD1 --- .../dts/allwinner/sun50i-h616-wirenboard85x.dtsi | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi index 9922bbbe9cf4c..de1427405d12c 100644 --- a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi +++ b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi @@ -831,7 +831,12 @@ compatible = "wirenboard,wbec-uart"; reg = <0>; - status = "disabled"; + status = "okay"; + + can_mod1: can@0 { + compatible = "holtek,ht42b416"; + current-speed = <115200>; + }; }; wbec_uart_mod2: serial@1 { @@ -1065,12 +1070,7 @@ pinctrl-0 = <&pinctrl_mod3_txrx_uart>; cts-override; - status = "okay"; - - can_mod3: can@0 { - compatible = "holtek,ht42b416"; - current-speed = <115200>; - }; + status = "disabled"; }; &uart5 { /* MOD4 */ From 33cbdf682de582ccbc0cb557b43e300b2dd3e1d6 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Sat, 27 Dec 2025 01:32:39 +0300 Subject: [PATCH 3/7] Add debug mode --- drivers/net/can/ht42b416.c | 47 +++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/drivers/net/can/ht42b416.c b/drivers/net/can/ht42b416.c index c8110a18aed96..ecc7474684760 100644 --- a/drivers/net/can/ht42b416.c +++ b/drivers/net/can/ht42b416.c @@ -37,6 +37,10 @@ #define HT42B416_POWERUP_DELAY_US 200 #define HT42B416_TX_TIMEOUT (2 * HZ) +static bool ht42b416_debug; +module_param_named(debug, ht42b416_debug, bool, 0644); +MODULE_PARM_DESC(debug, "Enable verbose UART logging"); + enum ht42b416_wait_ack { HT42B416_WAIT_NONE, HT42B416_WAIT_CR, @@ -95,6 +99,24 @@ static const u8 ht42b416_code_all_std[] = { 'M', 0x00, 0x00 }; static const u8 ht42b416_filter_all_ext[] = { 'm', 0x00, 0x00, 0x00, 0x00 }; static const u8 ht42b416_code_all_ext[] = { 'M', 0x00, 0x00, 0x00, 0x00 }; +static void ht42b416_log_uart(struct device *dev, const char *prefix, + const u8 *buf, size_t len) +{ + char hex[HT42B416_RX_BUF_LEN * 3 + 1]; + size_t i, pos = 0; + + for (i = 0; i < len && pos + 3 < sizeof(hex); i++) + pos += scnprintf(hex + pos, sizeof(hex) - pos, + "%02x ", buf[i]); + + if (pos) + hex[pos - 1] = '\0'; + else + hex[0] = '\0'; + + dev_info(dev, "%s[%zu]: %s\n", prefix, len, hex); +} + static int ht42b416_write_raw(struct ht42b416_priv *priv, const u8 *buf, size_t len) { @@ -128,6 +150,9 @@ static int ht42b416_send_cmd_locked(struct ht42b416_priv *priv, WRITE_ONCE(priv->wait_ack, wait); } + if (ht42b416_debug) + ht42b416_log_uart(&priv->serdev->dev, "UART TX cmd ", buffer, len); + ret = ht42b416_write_raw(priv, buffer, len); if (ret || wait == HT42B416_WAIT_NONE) goto out; @@ -240,6 +265,9 @@ static int ht42b416_hw_start(struct ht42b416_priv *priv) cmd[0] = 'S'; cmd[1] = code; + if (ht42b416_debug) + dev_info(&priv->serdev->dev, "CAN bitrate %u -> S 0x%02x\n", + priv->can.bittiming.bitrate, code); ret = ht42b416_send_cmd(priv, cmd, 2, HT42B416_WAIT_CR); if (ret) return ret; @@ -507,6 +535,14 @@ static void ht42b416_process_msg(struct ht42b416_priv *priv, { struct net_device *ndev = priv->can.dev; + if (ht42b416_debug) { + if (len) + ht42b416_log_uart(&priv->serdev->dev, + "UART RX msg ", buf, len); + else + dev_info(&priv->serdev->dev, "UART RX ack \n"); + } + if (!len) { ht42b416_signal_ack(priv); return; @@ -628,6 +664,10 @@ static netdev_tx_t ht42b416_start_xmit(struct sk_buff *skb, priv->tx_expect = eff ? HT42B416_TX_EXPECT_CAP_Z : HT42B416_TX_EXPECT_Z; priv->tx_dlc = cf->len; + if (ht42b416_debug) + ht42b416_log_uart(&priv->serdev->dev, "UART TX frame ", + priv->tx_buf, priv->tx_len); + netif_stop_queue(ndev); can_put_echo_skb(skb, ndev, 0, 0); @@ -752,7 +792,12 @@ static int ht42b416_probe(struct serdev_device *serdev) if (ret) goto err_free; - serdev_device_set_baudrate(serdev, priv->uart_baud); + ret = serdev_device_set_baudrate(serdev, priv->uart_baud); + if (ret > 0 && ret != priv->uart_baud) + dev_warn(dev, "UART requested %u baud, got %d\n", + priv->uart_baud, ret); + if (ret > 0) + priv->uart_baud = ret; serdev_device_set_flow_control(serdev, false); serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); From 744cfe58ea4687071f5d1b1080bb9b1583d1e916 Mon Sep 17 00:00:00 2001 From: Evgeny Boger Date: Sat, 27 Dec 2025 05:01:05 +0300 Subject: [PATCH 4/7] filters must be set after port is open, otherwise the IC will end up in some buggy state with a broken baudrate --- drivers/net/can/ht42b416.c | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/drivers/net/can/ht42b416.c b/drivers/net/can/ht42b416.c index ecc7474684760..9e85dd146cc23 100644 --- a/drivers/net/can/ht42b416.c +++ b/drivers/net/can/ht42b416.c @@ -253,6 +253,14 @@ static int ht42b416_hw_start(struct ht42b416_priv *priv) priv->rx_len = 0; + /* Reset chip just in case */ + cmd[0] = 'R'; + cmd[1] = 'S'; + cmd[2] = 'T'; + ret = ht42b416_send_cmd(priv, cmd, 3, HT42B416_WAIT_CR); + if (ret) + return ret; + /* Close device to ensure clean state */ cmd[0] = 'C'; ret = ht42b416_send_cmd(priv, cmd, 1, HT42B416_WAIT_CR); @@ -272,6 +280,20 @@ static int ht42b416_hw_start(struct ht42b416_priv *priv) if (ret) return ret; + if (priv->can.ctrlmode & CAN_CTRLMODE_LOOPBACK) + open_cmd = 'l'; + else if (priv->can.ctrlmode & CAN_CTRLMODE_LISTENONLY) + open_cmd = 'L'; + else + open_cmd = 'O'; + + ret = ht42b416_send_cmd(priv, &open_cmd, 1, HT42B416_WAIT_CR); + if (ret) + return ret; + + /* Filters must be set after port is open, otherwise the IC will + * end up in some buggy state with a broken baudrate + */ ret = ht42b416_send_cmd(priv, ht42b416_filter_all_std, sizeof(ht42b416_filter_all_std), HT42B416_WAIT_CR); @@ -296,17 +318,6 @@ static int ht42b416_hw_start(struct ht42b416_priv *priv) if (ret) return ret; - if (priv->can.ctrlmode & CAN_CTRLMODE_LOOPBACK) - open_cmd = 'l'; - else if (priv->can.ctrlmode & CAN_CTRLMODE_LISTENONLY) - open_cmd = 'L'; - else - open_cmd = 'O'; - - ret = ht42b416_send_cmd(priv, &open_cmd, 1, HT42B416_WAIT_CR); - if (ret) - return ret; - priv->tx_pos = 0; priv->tx_len = 0; priv->tx_expect = HT42B416_TX_NONE; From fff670afbec957f0f92c40e11257c14a46876416 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Fri, 16 Jan 2026 02:26:52 +0300 Subject: [PATCH 5/7] Disable CAN driver for overlay --- .../boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi index de1427405d12c..51983e30bd513 100644 --- a/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi +++ b/arch/arm64/boot/dts/allwinner/sun50i-h616-wirenboard85x.dtsi @@ -831,12 +831,7 @@ compatible = "wirenboard,wbec-uart"; reg = <0>; - status = "okay"; - - can_mod1: can@0 { - compatible = "holtek,ht42b416"; - current-speed = <115200>; - }; + status = "disabled"; }; wbec_uart_mod2: serial@1 { From 3a5b4f2b8ac4527ff53256dee920c5a9822d4766 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Fri, 16 Jan 2026 09:44:40 +0300 Subject: [PATCH 6/7] can: ht42b416: fix receive_buf signature for serdev --- drivers/net/can/ht42b416.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/drivers/net/can/ht42b416.c b/drivers/net/can/ht42b416.c index 9e85dd146cc23..29a4aa737138b 100644 --- a/drivers/net/can/ht42b416.c +++ b/drivers/net/can/ht42b416.c @@ -582,8 +582,8 @@ static void ht42b416_process_msg(struct ht42b416_priv *priv, } } -static size_t ht42b416_receive(struct serdev_device *serdev, - const u8 *data, size_t count) +static ssize_t ht42b416_receive(struct serdev_device *serdev, + const u8 *data, size_t count) { struct ht42b416_priv *priv = serdev_device_get_drvdata(serdev); size_t i; From 7f39bb4d7ebadb973f1ee3e4a3c2670c9bdd2891 Mon Sep 17 00:00:00 2001 From: Anton Tarasov Date: Fri, 16 Jan 2026 11:05:21 +0300 Subject: [PATCH 7/7] Bump version --- debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian/changelog b/debian/changelog index 5f2bfc20500be..8e83689f4781d 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +linux-wb (6.8.0-wb149) stable; urgency=medium + + * wbe2-i-can: add support for CAN-module based on Holtek ht42b41 + + -- Anton Tarasov Fri, 16 Jan 2026 11:05:02 +0300 + linux-wb (6.8.0-wb148) stable; urgency=medium * wb8.5 HDMI: reject >30Hz 4K modes on H616 since T507-H can’t drive 4K60 reliably