[PATCH v1 6/7] Bluetooth: hci_mediatek: Add protocol support for MediaTek serial devices
From: sean.wang
Date: Tue Apr 03 2018 - 03:16:54 EST
From: Sean Wang <sean.wang@xxxxxxxxxxxx>
This adds a driver for the MediaTek serial protocol based on H4 protocol,
which can enable the built-in Bluetooth device inside MT7622 SoC.
Signed-off-by: Sean Wang <sean.wang@xxxxxxxxxxxx>
---
drivers/bluetooth/Kconfig | 12 +
drivers/bluetooth/Makefile | 1 +
drivers/bluetooth/hci_mediatek.c | 499 +++++++++++++++++++++++++++++++++++++++
drivers/bluetooth/hci_uart.h | 3 +-
4 files changed, 514 insertions(+), 1 deletion(-)
create mode 100644 drivers/bluetooth/hci_mediatek.c
diff --git a/drivers/bluetooth/Kconfig b/drivers/bluetooth/Kconfig
index 010f5f5..851f430 100644
--- a/drivers/bluetooth/Kconfig
+++ b/drivers/bluetooth/Kconfig
@@ -104,6 +104,18 @@ config BT_HCIUART_H4
Say Y here to compile support for HCI UART (H4) protocol.
+config BT_HCIUART_MEDIATEK
+ tristate "MediaTek protocol support"
+ depends on BT_HCIUART_SERDEV
+ select BT_HCIUART_H4
+ help
+ The MediaTek protocol based on H4 enables patch firmware download and
+ configuration. This protocol is required for MediaTek Bluetooth
+ devices with a serial bus such as BTIF, which can be usually found on
+ various MediaTek SoCs.
+
+ Say Y here to compile support for MediaTek protocol.
+
config BT_HCIUART_NOKIA
tristate "UART Nokia H4+ protocol support"
depends on BT_HCIUART
diff --git a/drivers/bluetooth/Makefile b/drivers/bluetooth/Makefile
index ec16c55..db93c76 100644
--- a/drivers/bluetooth/Makefile
+++ b/drivers/bluetooth/Makefile
@@ -43,5 +43,6 @@ hci_uart-$(CONFIG_BT_HCIUART_INTEL) += hci_intel.o
hci_uart-$(CONFIG_BT_HCIUART_BCM) += hci_bcm.o
hci_uart-$(CONFIG_BT_HCIUART_QCA) += hci_qca.o
hci_uart-$(CONFIG_BT_HCIUART_AG6XX) += hci_ag6xx.o
+hci_uart-$(CONFIG_BT_HCIUART_MEDIATEK) += hci_mediatek.o
hci_uart-$(CONFIG_BT_HCIUART_MRVL) += hci_mrvl.o
hci_uart-objs := $(hci_uart-y)
diff --git a/drivers/bluetooth/hci_mediatek.c b/drivers/bluetooth/hci_mediatek.c
new file mode 100644
index 0000000..7ac1e7a
--- /dev/null
+++ b/drivers/bluetooth/hci_mediatek.c
@@ -0,0 +1,499 @@
+// SPDX-License-Identifier: GPL-2.0
+// Copyright (c) 2018 MediaTek Inc.
+
+/*
+ * Bluetooth HCI Serial driver for MediaTek SoC
+ *
+ * Author: Sean Wang <sean.wang@xxxxxxxxxxxx>
+ *
+ */
+
+#include <linux/clk.h>
+#include <linux/errno.h>
+#include <linux/firmware.h>
+#include <linux/kernel.h>
+#include <linux/module.h>
+#include <linux/of.h>
+#include <linux/pm_runtime.h>
+#include <linux/unaligned/le_struct.h>
+#include <linux/serdev.h>
+#include <linux/skbuff.h>
+#include <linux/types.h>
+#include <net/bluetooth/bluetooth.h>
+#include <net/bluetooth/hci_core.h>
+
+#include "hci_uart.h"
+
+#define FIRMWARE_MT7622 "mediatek/mt7622_patch_firmware.bin"
+
+#define MTK_STP_HDR_SIZE 4
+#define MTK_STP_TLR_SIZE 2
+#define MTK_WMT_HDR_SIZE 5
+#define MTK_WMT_CMD_SIZE (MTK_WMT_HDR_SIZE + MTK_STP_HDR_SIZE + \
+ MTK_STP_TLR_SIZE + HCI_ACL_HDR_SIZE)
+
+enum {
+ MTK_WMT_PATCH_DWNLD = 0x1,
+ MTK_WMT_FUNC_CTRL = 0x6,
+ MTK_WMT_RST = 0x7
+};
+
+struct mtk_stp_splitter {
+ u8 pad[6];
+ u8 cursor;
+ u16 dlen;
+};
+
+struct mtk_bt_dev {
+ struct hci_uart hu;
+ struct clk *clk;
+ struct sk_buff *rx_skb;
+ struct sk_buff_head txq;
+ struct completion wmt_cmd;
+ struct mtk_stp_splitter *sp;
+};
+
+struct mtk_stp_hdr {
+ __u8 prefix;
+ __u8 dlen1:4;
+ __u8 type:4;
+ __u8 dlen2:8;
+ __u8 cs;
+} __packed;
+
+struct mtk_wmt_hdr {
+ __u8 dir;
+ __u8 op;
+ __le16 dlen;
+ __u8 flag;
+} __packed;
+
+static void mtk_stp_reset(struct mtk_stp_splitter *sp)
+{
+ sp->cursor = 2;
+ sp->dlen = 0;
+}
+
+static const unsigned char *
+mtk_stp_split(struct device *dev, struct mtk_stp_splitter *sp,
+ const unsigned char *data, int count, int *sz_h4)
+{
+ struct mtk_stp_hdr *shdr;
+
+ /* The cursor is reset when all the data of STP is being consumed. */
+ if (!sp->dlen && sp->cursor >= 6)
+ sp->cursor = 0;
+
+ /* Filling pad until all STP info is obtained. */
+ while (sp->cursor < 6 && count > 0) {
+ sp->pad[sp->cursor] = *data;
+ sp->cursor++;
+ data++;
+ count--;
+ }
+
+ /* Retrieve STP info and have a sanity check. */
+ if (!sp->dlen && sp->cursor >= 6) {
+ shdr = (struct mtk_stp_hdr *)&sp->pad[2];
+ sp->dlen = shdr->dlen1 << 8 | shdr->dlen2;
+
+ /* Resync STP when unexpected data is being read. */
+ if (shdr->prefix != 0x80 || sp->dlen > 2048) {
+ dev_err(dev, "stp format unexpect (%d, %d)",
+ shdr->prefix, sp->dlen);
+ mtk_stp_reset(sp);
+ }
+ }
+
+ /* Directly leave when there's no data found for H4 can process. */
+ if (count <= 0)
+ return NULL;
+
+ /* Tranlate to how much the size of data H4 can handle so far. */
+ *sz_h4 = min_t(int, count, sp->dlen);
+ /* Update remaining the size of STP packet. */
+ sp->dlen -= *sz_h4;
+
+ /* Data points to STP payload which can be handled by H4. */
+ return data;
+}
+
+static void mtk_stp_hdr_build(struct mtk_stp_hdr *shdr, u8 type, u32 dlen)
+{
+ __u8 *p = (__u8 *)shdr;
+
+ shdr->prefix = 0x80;
+ shdr->dlen1 = (dlen & 0xf00) >> 8;
+ shdr->type = type;
+ shdr->dlen2 = dlen & 0xff;
+ shdr->cs = (p[0] + p[1] + p[2]) & 0xff;
+}
+
+static int mtk_enqueue(struct hci_uart *hu, struct sk_buff *skb)
+{
+ struct mtk_bt_dev *btdev = hu->priv;
+ struct mtk_stp_hdr *shdr;
+ struct sk_buff *new_skb;
+ int dlen;
+
+ memcpy(skb_push(skb, 1), &hci_skb_pkt_type(skb), 1);
+ dlen = skb->len;
+
+ /* Make sure of STP header at least has 4-bytes free space to fill. */
+ if (unlikely(skb_headroom(skb) < MTK_STP_HDR_SIZE)) {
+ new_skb = skb_realloc_headroom(skb, MTK_STP_HDR_SIZE);
+ kfree_skb(skb);
+ skb = new_skb;
+ }
+
+ /* Build for STP packet format. */
+ shdr = skb_push(skb, MTK_STP_HDR_SIZE);
+ mtk_stp_hdr_build(shdr, 0, dlen);
+ skb_put_zero(skb, MTK_STP_TLR_SIZE);
+
+ skb_queue_tail(&btdev->txq, skb);
+
+ return 0;
+}
+
+static int mtk_wmt_cmd_sync(struct hci_uart *hu, u8 opcode, u8 flag, u16 plen,
+ const void *param)
+{
+ struct mtk_bt_dev *btdev = hu->priv;
+ struct hci_command_hdr *hhdr;
+ struct hci_acl_hdr *ahdr;
+ struct mtk_wmt_hdr *whdr;
+ struct sk_buff *skb;
+ int ret = 0;
+
+ init_completion(&btdev->wmt_cmd);
+
+ skb = bt_skb_alloc(plen + MTK_WMT_CMD_SIZE, GFP_KERNEL);
+ if (!skb)
+ return -ENOMEM;
+
+ /*
+ * WMT data is carried in either ACL or HCI format with op code as
+ * 0xfc6f and followed by a WMT header and its actual payload.
+ */
+ switch (opcode) {
+ case MTK_WMT_PATCH_DWNLD:
+ ahdr = skb_put(skb, HCI_ACL_HDR_SIZE);
+ ahdr->handle = cpu_to_le16(0xfc6f);
+ ahdr->dlen = cpu_to_le16(plen + MTK_WMT_HDR_SIZE);
+ break;
+ default:
+ hhdr = skb_put(skb, HCI_COMMAND_HDR_SIZE);
+ hhdr->opcode = cpu_to_le16(0xfc6f);
+ hhdr->plen = plen + MTK_WMT_HDR_SIZE;
+ break;
+ }
+
+ hci_skb_pkt_type(skb) = opcode == MTK_WMT_PATCH_DWNLD ?
+ HCI_ACLDATA_PKT : HCI_COMMAND_PKT;
+
+ /* Start to build a WMT header and its actual payload. */
+ whdr = skb_put(skb, MTK_WMT_HDR_SIZE);
+ whdr->dir = 1;
+ whdr->op = opcode;
+ whdr->dlen = cpu_to_le16(plen + 1);
+ whdr->flag = flag;
+ skb_put_data(skb, param, plen);
+
+ mtk_enqueue(hu, skb);
+ hci_uart_tx_wakeup(hu);
+
+ /*
+ * Waiting a WMT event response, while we must take care in case of
+ * failures for the wait.
+ */
+ ret = wait_for_completion_interruptible_timeout(&btdev->wmt_cmd, HZ);
+
+ return ret > 0 ? 0 : ret < 0 ? ret : -ETIMEDOUT;
+}
+
+static int mtk_setup_fw(struct hci_uart *hu)
+{
+ struct mtk_bt_dev *btdev = hu->priv;
+ const struct firmware *fw;
+ struct device *dev;
+ const char *fwname;
+ const u8 *fw_ptr;
+ size_t fw_size;
+ int err, dlen;
+ u8 flag;
+
+ dev = &hu->serdev->dev;
+ fwname = FIRMWARE_MT7622;
+
+ init_completion(&btdev->wmt_cmd);
+
+ err = request_firmware(&fw, fwname, dev);
+ if (err < 0) {
+ dev_err(dev, "%s: Failed to load firmware file (%d)",
+ hu->hdev->name, err);
+ return err;
+ }
+
+ fw_ptr = fw->data;
+ fw_size = fw->size;
+
+ /* The size of a patch header at least has 30 bytes. */
+ if (fw_size < 30)
+ return -EINVAL;
+
+ while (fw_size > 0) {
+ dlen = min_t(int, 1000, fw_size);
+
+ /* Tell deivice the position in sequence. */
+ flag = (fw_size - dlen <= 0) ? 3 :
+ (fw_size < fw->size) ? 2 : 1;
+
+ err = mtk_wmt_cmd_sync(hu, MTK_WMT_PATCH_DWNLD, flag,
+ dlen, fw_ptr);
+ if (err < 0)
+ break;
+
+ fw_size -= dlen;
+ fw_ptr += dlen;
+ }
+
+ release_firmware(fw);
+
+ return err;
+}
+
+static int mtk_open(struct hci_uart *hu)
+{
+ struct mtk_bt_dev *btdev = hu->priv;
+ struct device *dev;
+ int err = 0;
+
+ dev = &hu->serdev->dev;
+
+ serdev_device_open(hu->serdev);
+ skb_queue_head_init(&btdev->txq);
+
+ /* Setup the usage of H4. */
+ hu->alignment = 1;
+ hu->padding = 0;
+ mtk_stp_reset(btdev->sp);
+
+ /* Enable the power domain and clock the device requires */
+ pm_runtime_enable(dev);
+ err = pm_runtime_get_sync(dev);
+ if (err < 0) {
+ pm_runtime_disable(dev);
+ return err;
+ }
+
+ err = clk_prepare_enable(btdev->clk);
+ if (err < 0) {
+ pm_runtime_put_sync(dev);
+ pm_runtime_disable(dev);
+ }
+
+ return err;
+}
+
+static int mtk_close(struct hci_uart *hu)
+{
+ struct mtk_bt_dev *btdev = hu->priv;
+ struct device *dev = &hu->serdev->dev;
+ u8 param = 0x0;
+
+ skb_queue_purge(&btdev->txq);
+ kfree_skb(btdev->rx_skb);
+
+ /* Disable the device. */
+ mtk_wmt_cmd_sync(hu, MTK_WMT_FUNC_CTRL, 0x0, sizeof(param), ¶m);
+
+ /* Shutdown the clock and power domain the device requires. */
+ clk_disable_unprepare(btdev->clk);
+
+ pm_runtime_put_sync(dev);
+ pm_runtime_disable(dev);
+
+ serdev_device_close(hu->serdev);
+
+ return 0;
+}
+
+int mtk_recv_frame(struct hci_dev *hdev, struct sk_buff *skb)
+{
+ struct hci_event_hdr *hdr = (void *)skb->data;
+ struct hci_uart *hu = hci_get_drvdata(hdev);
+ struct mtk_bt_dev *btdev = hu->priv;
+
+ if (hci_skb_pkt_type(skb) == HCI_EVENT_PKT &&
+ hdr->evt == 0xe4) {
+ complete(&btdev->wmt_cmd);
+ kfree_skb(skb);
+ return 0;
+ }
+
+ return hci_recv_frame(hdev, skb);
+}
+
+static const struct h4_recv_pkt mtk_recv_pkts[] = {
+ { H4_RECV_ACL, .recv = mtk_recv_frame },
+ { H4_RECV_SCO, .recv = mtk_recv_frame },
+ { H4_RECV_EVENT, .recv = mtk_recv_frame },
+};
+
+static int mtk_recv(struct hci_uart *hu, const void *data, int count)
+{
+ const unsigned char *p_left = data, *p_h4;
+ struct mtk_bt_dev *btdev = hu->priv;
+ int sz_left = count, sz_h4, adv;
+ struct device *dev;
+ int err;
+
+ if (!test_bit(HCI_UART_REGISTERED, &hu->flags))
+ return -EUNATCH;
+
+ dev = &hu->serdev->dev;
+
+ while (sz_left > 0) {
+ p_h4 = mtk_stp_split(dev, btdev->sp, p_left, sz_left, &sz_h4);
+ if (!p_h4)
+ break;
+
+ adv = p_h4 - p_left;
+ sz_left -= adv;
+ p_left += adv;
+
+ btdev->rx_skb = h4_recv_buf(hu->hdev, btdev->rx_skb,
+ p_h4, sz_h4, mtk_recv_pkts,
+ ARRAY_SIZE(mtk_recv_pkts));
+ if (IS_ERR(btdev->rx_skb)) {
+ err = PTR_ERR(btdev->rx_skb);
+ dev_err(dev, "Frame reassembly failed (%d)", err);
+ btdev->rx_skb = NULL;
+ return err;
+ }
+
+ sz_left -= sz_h4;
+ p_left += sz_h4;
+ }
+
+ return count;
+}
+
+static struct sk_buff *mtk_dequeue(struct hci_uart *hu)
+{
+ struct mtk_bt_dev *btdev = hu->priv;
+
+ return skb_dequeue(&btdev->txq);
+}
+
+static int mtk_flush(struct hci_uart *hu)
+{
+ struct mtk_bt_dev *btdev = hu->priv;
+
+ skb_queue_purge(&btdev->txq);
+
+ return 0;
+}
+
+static int mtk_setup(struct hci_uart *hu)
+{
+ struct device *dev;
+ u8 param = 0x1;
+ int err;
+
+ dev = &hu->serdev->dev;
+
+ /* Setup a firmware which the device definitely requires. */
+ err = mtk_setup_fw(hu);
+ if (err < 0) {
+ dev_err(dev, "Fail at setup FW (%d): %d", __LINE__, err);
+ return err;
+ }
+
+ /* Activate funciton the firmware provides to. */
+ err = mtk_wmt_cmd_sync(hu, MTK_WMT_RST, 0x4, 0, 0);
+ if (err < 0) {
+ dev_err(dev, "Fail at WMT RST (%d): %d", __LINE__, err);
+ return err;
+ }
+
+ /* Enable Bluetooth protocol. */
+ err = mtk_wmt_cmd_sync(hu, MTK_WMT_FUNC_CTRL, 0x0, sizeof(param),
+ ¶m);
+ if (err < 0)
+ dev_err(dev, "Fail at FUNC CTRL (%d): %d", __LINE__, err);
+
+ return err;
+}
+
+static const struct hci_uart_proto mtk_proto = {
+ .id = HCI_UART_MTK,
+ .name = "MediaTek",
+ .open = mtk_open,
+ .close = mtk_close,
+ .recv = mtk_recv,
+ .enqueue = mtk_enqueue,
+ .dequeue = mtk_dequeue,
+ .flush = mtk_flush,
+ .setup = mtk_setup,
+ .manufacturer = 1,
+};
+
+static int mtk_bluetooth_serdev_probe(struct serdev_device *serdev)
+{
+ struct device *dev = &serdev->dev;
+ struct mtk_bt_dev *btdev;
+ int err = 0;
+
+ btdev = devm_kzalloc(dev, sizeof(*btdev), GFP_KERNEL);
+ if (!btdev)
+ return -ENOMEM;
+
+ btdev->sp = devm_kzalloc(dev, sizeof(*btdev->sp), GFP_KERNEL);
+ if (!btdev->sp)
+ return -ENOMEM;
+
+ btdev->clk = devm_clk_get(dev, "ref");
+ if (IS_ERR(btdev->clk))
+ return PTR_ERR(btdev->clk);
+
+ btdev->hu.serdev = serdev;
+ btdev->hu.priv = btdev;
+
+ serdev_device_set_drvdata(serdev, btdev);
+
+ err = hci_uart_register_device(&btdev->hu, &mtk_proto);
+ if (err)
+ dev_err(dev, "Could not register bluetooth uart: %d", err);
+
+ return err;
+}
+
+static void mtk_bluetooth_serdev_remove(struct serdev_device *serdev)
+{
+ struct mtk_bt_dev *btdev = serdev_device_get_drvdata(serdev);
+
+ hci_uart_unregister_device(&btdev->hu);
+}
+
+static const struct of_device_id mtk_bluetooth_of_match[] = {
+ { .compatible = "mediatek,mt7622-bluetooth" },
+ {},
+};
+MODULE_DEVICE_TABLE(of, mtk_bluetooth_of_match);
+
+static struct serdev_device_driver mtk_bluetooth_serdev_driver = {
+ .probe = mtk_bluetooth_serdev_probe,
+ .remove = mtk_bluetooth_serdev_remove,
+ .driver = {
+ .name = "mediatek-bluetooth",
+ .of_match_table = of_match_ptr(mtk_bluetooth_of_match),
+ },
+};
+
+module_serdev_device_driver(mtk_bluetooth_serdev_driver);
+
+MODULE_AUTHOR("Sean Wang <sean.wang@xxxxxxxxxxxx>");
+MODULE_DESCRIPTION("Bluetooth HCI Serial driver for MediaTek SoC");
+MODULE_LICENSE("GPL v2");
diff --git a/drivers/bluetooth/hci_uart.h b/drivers/bluetooth/hci_uart.h
index 66e8c68..3f0911e 100644
--- a/drivers/bluetooth/hci_uart.h
+++ b/drivers/bluetooth/hci_uart.h
@@ -35,7 +35,7 @@
#define HCIUARTGETFLAGS _IOR('U', 204, int)
/* UART protocols */
-#define HCI_UART_MAX_PROTO 12
+#define HCI_UART_MAX_PROTO 13
#define HCI_UART_H4 0
#define HCI_UART_BCSP 1
@@ -49,6 +49,7 @@
#define HCI_UART_AG6XX 9
#define HCI_UART_NOKIA 10
#define HCI_UART_MRVL 11
+#define HCI_UART_MTK 12
#define HCI_UART_RAW_DEVICE 0
#define HCI_UART_RESET_ON_INIT 1
--
2.7.4