| /* |
| * Copyright (c) 2020 The Linux Foundation. All rights reserved. |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 and |
| * only version 2 as published by the Free Software Foundation. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/of.h> |
| #include <linux/io.h> |
| #include <linux/init.h> |
| #include <linux/slab.h> |
| #include <linux/list.h> |
| #include <linux/errno.h> |
| #include <linux/delay.h> |
| #include <linux/types.h> |
| #include <linux/kernel.h> |
| #include <linux/device.h> |
| #include <linux/module.h> |
| #include <linux/regmap.h> |
| #include <linux/of_irq.h> |
| #include <linux/kthread.h> |
| #include <linux/debugfs.h> |
| #include <linux/notifier.h> |
| #include <linux/mfd/syscon.h> |
| #include <uapi/linux/major.h> |
| #include <linux/completion.h> |
| #include <linux/ipc_logging.h> |
| #include <linux/workqueue.h> |
| #include <linux/qcom_scm.h> |
| #include <linux/bt.h> |
| |
| void bt_ipc_purge_tx_queue(struct bt_descriptor *btDesc) |
| { |
| struct ipc_intent *intent; |
| |
| while (!list_empty(&btDesc->ipc.tx_q)) { |
| intent = list_first_entry(&btDesc->ipc.tx_q, struct ipc_intent, |
| list); |
| |
| list_del(&intent->list); |
| kfree(intent->buf); |
| kfree(intent); |
| } |
| |
| atomic_set(&btDesc->ipc.tx_q_cnt, 0); |
| } |
| EXPORT_SYMBOL(bt_ipc_purge_tx_queue); |
| |
| static void bt_ipc_process_pending_tx_queue(struct bt_descriptor *btDesc) |
| { |
| int ret; |
| struct ipc_intent *intent; |
| |
| while (!list_empty(&btDesc->ipc.tx_q)) { |
| intent = list_first_entry(&btDesc->ipc.tx_q, struct ipc_intent, |
| list); |
| |
| ret = bt_ipc_send_msg(btDesc, (uint8_t)0x100, intent->buf, |
| intent->len, false); |
| if (ret) { |
| mdelay(50); |
| break; |
| } |
| |
| list_del(&intent->list); |
| atomic_dec(&btDesc->ipc.tx_q_cnt); |
| kfree(intent->buf); |
| kfree(intent); |
| } |
| } |
| |
| int |
| bt_ipc_queue_tx(struct bt_descriptor *btDesc, const uint8_t *buf, uint16_t len) |
| { |
| struct ipc_intent *intent; |
| uint8_t *buffer; |
| struct device *dev = &btDesc->pdev->dev; |
| |
| if (unlikely(atomic_read(&btDesc->ipc.tx_q_cnt) >= IPC_TX_QSIZE)) { |
| dev_err(dev, "TX Queue Limit reached\n"); |
| return -ENOSPC; |
| } |
| |
| intent = kzalloc(sizeof(struct ipc_intent), GFP_KERNEL); |
| if (!intent) |
| return -ENOMEM; |
| |
| buffer = kzalloc(len, GFP_KERNEL); |
| if (!buffer) |
| return -ENOMEM; |
| |
| memcpy_toio(buffer, buf, len); |
| intent->buf = buffer; |
| |
| intent->len = len; |
| list_add_tail(&intent->list, &btDesc->ipc.tx_q); |
| atomic_inc(&btDesc->ipc.tx_q_cnt); |
| |
| return 0; |
| } |
| |
| static void *bt_ipc_alloc_lmsg(struct bt_descriptor *btDesc, uint32_t len, |
| struct ipc_aux_ptr *aux_ptr, uint8_t *is_lbuf_full) |
| { |
| uint8_t idx; |
| uint8_t blks; |
| uint8_t blks_consumed; |
| struct bt_mem *btmem = &btDesc->btmem; |
| struct device *dev = &btDesc->pdev->dev; |
| uint32_t lsz = IPC_LBUF_SZ(btmem->tx_ctxt, TotalMemorySize, lring_buf, |
| lmsg_buf_cnt); |
| |
| if (btmem->tx_ctxt->lring_buf == 0) { |
| dev_err(dev, "no long message buffer not initialized\n"); |
| return ERR_PTR(-ENODEV); |
| } |
| |
| blks = GET_NO_OF_BLOCKS(len, lsz); |
| |
| if (!btmem->lmsg_ctxt.lmsg_free_cnt || |
| (blks > btmem->lmsg_ctxt.lmsg_free_cnt)) |
| return ERR_PTR(-EAGAIN); |
| |
| idx = btmem->lmsg_ctxt.widx; |
| |
| if ((btmem->lmsg_ctxt.widx + blks) > btmem->tx_ctxt->lmsg_buf_cnt) { |
| blks_consumed = btmem->tx_ctxt->lmsg_buf_cnt - idx; |
| aux_ptr->len = len - (blks_consumed * lsz); |
| aux_ptr->buf = btmem->tx_ctxt->lring_buf; |
| } |
| |
| btmem->lmsg_ctxt.widx = (btmem->lmsg_ctxt.widx + blks) % |
| btmem->tx_ctxt->lmsg_buf_cnt; |
| |
| btmem->lmsg_ctxt.lmsg_free_cnt -= blks; |
| |
| if (btmem->lmsg_ctxt.lmsg_free_cnt <= |
| ((btmem->tx_ctxt->lmsg_buf_cnt * 20) / 100)) |
| *is_lbuf_full = 1; |
| |
| return (TO_APPS_ADDR(btmem->tx_ctxt->lring_buf) + (idx * lsz)); |
| } |
| |
| static struct ring_buffer_info *bt_ipc_get_tx_rbuf(struct bt_descriptor *btDesc, |
| uint8_t *is_sbuf_full) |
| { |
| uint8_t idx; |
| struct ring_buffer_info *rinfo; |
| struct bt_mem *btmem = &btDesc->btmem; |
| |
| for (rinfo = &(btmem->tx_ctxt->sring_buf_info); rinfo != NULL; |
| rinfo = (struct ring_buffer_info *)(uintptr_t)(rinfo->next)) { |
| idx = (rinfo->widx + 1) % (btmem->tx_ctxt->smsg_buf_cnt); |
| |
| if (idx != rinfo->tidx) { |
| btmem->lmsg_ctxt.smsg_free_cnt--; |
| |
| if (btmem->lmsg_ctxt.smsg_free_cnt <= |
| ((btmem->tx_ctxt->smsg_buf_cnt * 20) / 100)) |
| *is_sbuf_full = 1; |
| |
| return rinfo; |
| } |
| } |
| |
| return ERR_PTR(-EAGAIN); |
| } |
| |
| int bt_ipc_send_msg(struct bt_descriptor *btDesc, uint16_t msg_hdr, |
| const uint8_t *pData, uint16_t len, bool dequeue) |
| { |
| int ret = 0; |
| struct bt_mem *btmem = &btDesc->btmem; |
| struct device *dev = &btDesc->pdev->dev; |
| struct ring_buffer_info *rinfo; |
| struct ring_buffer *rbuf; |
| uint8_t is_lbuf_full = 0; |
| uint8_t is_sbuf_full = 0; |
| struct ipc_aux_ptr aux_ptr; |
| void *lmsg_data; |
| |
| if (dequeue) |
| bt_ipc_process_pending_tx_queue(btDesc); |
| |
| rinfo = bt_ipc_get_tx_rbuf(btDesc, &is_sbuf_full); |
| if (IS_ERR(rinfo)) { |
| dev_err(dev, "short msg buf full, queuing msg[%d]\n", |
| atomic_read(&btDesc->ipc.tx_q_cnt)); |
| ret = PTR_ERR(rinfo); |
| if (dequeue) |
| ret = bt_ipc_queue_tx(btDesc, pData, len); |
| return ret; |
| } |
| |
| rbuf = &((struct ring_buffer *)(TO_APPS_ADDR( |
| rinfo->rbuf)))[rinfo->widx]; |
| rbuf->msg_hdr = msg_hdr; |
| rbuf->len = len; |
| |
| if (len > IPC_MSG_PLD_SZ) { |
| rbuf->msg_hdr = rbuf->msg_hdr | IPC_LMSG_MASK; |
| |
| aux_ptr.len = 0; |
| aux_ptr.buf = 0; |
| |
| lmsg_data = bt_ipc_alloc_lmsg(btDesc, len, |
| &aux_ptr, &is_lbuf_full); |
| |
| if (IS_ERR(lmsg_data)) { |
| dev_err(dev, "long msg buf full, queuing msg[%d]\n", |
| atomic_read(&btDesc->ipc.tx_q_cnt)); |
| ret = PTR_ERR(lmsg_data); |
| if (dequeue) |
| ret = bt_ipc_queue_tx(btDesc, pData, len); |
| return ret; |
| } |
| |
| memcpy_toio(lmsg_data, pData, |
| (len - aux_ptr.len)); |
| |
| if (aux_ptr.buf) { |
| memcpy_toio(TO_APPS_ADDR(aux_ptr.buf), |
| (pData + (len - aux_ptr.len)), aux_ptr.len); |
| } |
| |
| rbuf->payload.lmsg_data = TO_BT_ADDR(lmsg_data); |
| } else { |
| memcpy_toio(rbuf->payload.smsg_data, pData, len); |
| } |
| |
| if (is_sbuf_full || is_lbuf_full) |
| rbuf->msg_hdr = rbuf->msg_hdr | IPC_RACK_MASK; |
| |
| rinfo->widx = (rinfo->widx + 1) % btmem->tx_ctxt->smsg_buf_cnt; |
| |
| regmap_write(btDesc->ipc.regmap, btDesc->ipc.offset, |
| BIT(btDesc->ipc.bit)); |
| |
| return ret; |
| } |
| |
| static |
| void bt_ipc_free_lmsg(struct bt_descriptor *btDesc, uint32_t lmsg, uint16_t len) |
| { |
| uint8_t idx; |
| uint8_t blks; |
| struct bt_mem *btmem = &btDesc->btmem; |
| uint32_t lsz = IPC_LBUF_SZ(btmem->tx_ctxt, TotalMemorySize, lring_buf, |
| lmsg_buf_cnt); |
| |
| idx = GET_TX_INDEX_FROM_BUF(lmsg, lsz); |
| |
| if (idx != btmem->lmsg_ctxt.ridx) |
| return; |
| |
| blks = GET_NO_OF_BLOCKS(len, lsz); |
| |
| btmem->lmsg_ctxt.ridx = (btmem->lmsg_ctxt.ridx + blks) % |
| btmem->tx_ctxt->lmsg_buf_cnt; |
| |
| btmem->lmsg_ctxt.lmsg_free_cnt += blks; |
| } |
| |
| static void bt_ipc_cust_msg(struct bt_descriptor *btDesc, uint8_t msgid) |
| { |
| struct device *dev = &btDesc->pdev->dev; |
| struct bt_mem *btmem = &btDesc->btmem; |
| uint16_t msg_hdr = 0; |
| int ret; |
| |
| msg_hdr |= msgid; |
| |
| switch (msgid) { |
| case IPC_CMD_IPC_STOP: |
| spin_unlock(&btDesc->lock); |
| ret = qti_scm_toggle_bt_eco(PAS_ID, 0x4); |
| spin_lock(&btDesc->lock); |
| if (ret) { |
| dev_err(dev, "Failed to set BT ECO\n"); |
| return; |
| } |
| |
| dev_info(dev, "BT IPC Stopped, gracefully stopping APSS IPC\n"); |
| break; |
| case IPC_CMD_SWITCH_TO_UART: |
| dev_info(dev, "Configured UART, Swithing BT to debug mode\n"); |
| break; |
| case IPC_CMD_PREPARE_DUMP: |
| dev_info(dev, "IPQ crashed, inform BT to prepare dump\n"); |
| break; |
| case IPC_CMD_COLLECT_DUMP: |
| dev_info(dev, "BT Crashed, gracefully stopping IPC\n"); |
| return; |
| case IPC_CMD_IPC_START: |
| spin_unlock(&btDesc->lock); |
| ret = qti_scm_toggle_bt_eco(PAS_ID, 0x0); |
| spin_lock(&btDesc->lock); |
| if (ret) { |
| dev_err(dev, "Failed to reset BT ECO\n"); |
| return; |
| } |
| |
| btmem->tx_ctxt = (struct context_info *)((void *) |
| btmem->rx_ctxt + btmem->rx_ctxt->TotalMemorySize); |
| btmem->lmsg_ctxt.widx = 0; |
| btmem->lmsg_ctxt.ridx = 0; |
| btmem->lmsg_ctxt.smsg_free_cnt = btmem->tx_ctxt->smsg_buf_cnt; |
| btmem->lmsg_ctxt.lmsg_free_cnt = btmem->tx_ctxt->lmsg_buf_cnt; |
| atomic_set(&btDesc->state, 1); |
| |
| dev_info(dev, "BT IPC Started, starting APSS IPC\n"); |
| return; |
| default: |
| dev_err(dev, "invalid custom message\n"); |
| return; |
| } |
| |
| if (unlikely(!atomic_read(&btDesc->state))) { |
| dev_err(dev, "BT IPC not initialized, no message sent\n"); |
| return; |
| } |
| |
| atomic_set(&btDesc->state, 0); |
| |
| ret = bt_ipc_send_msg(btDesc, msg_hdr, NULL, 0, true); |
| if (ret) |
| dev_err(dev, "err: sending message\n"); |
| } |
| |
| static bool bt_ipc_process_peer_msgs(struct bt_descriptor *btDesc, |
| struct ring_buffer_info *rinfo, uint8_t *pRxMsgCount) |
| { |
| struct bt_mem *btmem = &btDesc->btmem; |
| struct ring_buffer *rbuf; |
| uint8_t ridx, lbuf_idx; |
| uint8_t blks_consumed; |
| struct ipc_aux_ptr aux_ptr; |
| enum ipc_pkt_type pktType = IPC_CUST_PKT; |
| bool ackReqd = false; |
| uint8_t *rxbuf = NULL; |
| unsigned char *buf; |
| uint32_t lsz = IPC_LBUF_SZ(btmem->rx_ctxt, TotalMemorySize, lring_buf, |
| lmsg_buf_cnt); |
| |
| ridx = rinfo->ridx; |
| |
| rbuf = &((struct ring_buffer *)(TO_APPS_ADDR( |
| btmem->rx_ctxt->sring_buf_info.rbuf)))[ridx]; |
| |
| while (ridx != rinfo->widx) { |
| memset(&aux_ptr, 0, sizeof(struct ipc_aux_ptr)); |
| |
| rbuf = &((struct ring_buffer *)(TO_APPS_ADDR( |
| btmem->rx_ctxt->sring_buf_info.rbuf)))[ridx]; |
| |
| if (IS_LONG_MSG(rbuf->msg_hdr)) { |
| rxbuf = TO_APPS_ADDR(rbuf->payload.lmsg_data); |
| |
| if (IS_RX_MEM_NON_CONTIGIOUS(rbuf->payload.lmsg_data, |
| rbuf->len, lsz)) { |
| |
| lbuf_idx = GET_RX_INDEX_FROM_BUF( |
| rbuf->payload.lmsg_data, lsz); |
| |
| blks_consumed = btmem->rx_ctxt->lmsg_buf_cnt - |
| lbuf_idx; |
| aux_ptr.len = rbuf->len - (blks_consumed * lsz); |
| aux_ptr.buf = btmem->rx_ctxt->lring_buf; |
| } |
| } else { |
| rxbuf = rbuf->payload.smsg_data; |
| } |
| |
| if (IS_REQ_ACK(rbuf->msg_hdr)) |
| ackReqd = true; |
| |
| pktType = IPC_GET_PKT_TYPE(rbuf->msg_hdr); |
| |
| switch (pktType) { |
| case IPC_HCI_PKT: |
| buf = kzalloc(rbuf->len, GFP_ATOMIC); |
| if (!buf) |
| return -ENOMEM; |
| |
| memcpy_fromio(buf, rxbuf, (rbuf->len - aux_ptr.len)); |
| |
| if (aux_ptr.buf) |
| memcpy_fromio(buf + (rbuf->len - aux_ptr.len), |
| TO_APPS_ADDR(aux_ptr.buf), aux_ptr.len); |
| |
| btDesc->recvmsg_cb(btDesc, buf, rbuf->len); |
| kfree(buf); |
| break; |
| case IPC_CUST_PKT: |
| bt_ipc_cust_msg(btDesc, IPC_GET_MSG_ID(rbuf->msg_hdr)); |
| break; |
| case IPC_AUDIO_PKT: |
| break; |
| default: |
| break; |
| } |
| |
| ridx = (ridx + 1) % rinfo->ring_buf_cnt; |
| } |
| |
| spin_unlock(&btDesc->lock); |
| wait_event(btDesc->ipc.wait_q, |
| ((2 * rbuf->len) < bt_ipc_avail_size(btDesc))); |
| spin_lock(&btDesc->lock); |
| rinfo->ridx = ridx; |
| |
| return ackReqd; |
| } |
| |
| static void bt_ipc_process_ack(struct bt_descriptor *btDesc) |
| { |
| struct ring_buffer_info *rinfo; |
| struct bt_mem *btmem = &btDesc->btmem; |
| |
| for (rinfo = &btmem->tx_ctxt->sring_buf_info; rinfo != NULL; |
| rinfo = (struct ring_buffer_info *)(uintptr_t)(rinfo->next)) { |
| uint8_t tidx = rinfo->tidx; |
| struct ring_buffer *rbuf = (struct ring_buffer *) |
| TO_APPS_ADDR(rinfo->rbuf); |
| |
| while (tidx != rinfo->ridx) { |
| if (IS_LONG_MSG(rbuf[tidx].msg_hdr)) { |
| bt_ipc_free_lmsg(btDesc, |
| rbuf[tidx].payload.lmsg_data, |
| rbuf[tidx].len); |
| } |
| |
| tidx = (tidx + 1) % btmem->tx_ctxt->smsg_buf_cnt; |
| btmem->lmsg_ctxt.smsg_free_cnt++; |
| } |
| |
| rinfo->tidx = tidx; |
| } |
| } |
| |
| static |
| int bt_ipc_sendmsg_ex(struct bt_descriptor *btDesc, uint16_t msg_hdr, unsigned char *buf, int len) |
| { |
| int ret; |
| struct device *dev = &btDesc->pdev->dev; |
| |
| if (unlikely(!atomic_read(&btDesc->state))) { |
| dev_err(dev, "BT IPC not initialized, no message sent\n"); |
| return -ENODEV; |
| } |
| |
| spin_lock(&btDesc->lock); |
| |
| ret = bt_ipc_send_msg(btDesc, msg_hdr, (uint8_t *)buf, (uint16_t)len, |
| true); |
| if (ret) |
| dev_err(dev, "err: sending message\n"); |
| |
| spin_unlock(&btDesc->lock); |
| |
| return ret; |
| } |
| |
| static |
| int bt_ipc_sendmsg(struct bt_descriptor *btDesc, unsigned char *buf, int len) |
| { |
| const uint16_t hci_msg_hdr = 0x100; |
| return bt_ipc_sendmsg_ex(btDesc, hci_msg_hdr, buf, len); |
| } |
| |
| static uint32_t RX_CTXT_OFFSET = 0xe000; |
| |
| static void bt_ipc_worker(struct work_struct *work) |
| { |
| struct ring_buffer_info *rinfo; |
| |
| struct bt_ipc *ipc = container_of(work, struct bt_ipc, work); |
| struct bt_descriptor *btDesc = container_of(ipc, struct bt_descriptor, |
| ipc); |
| struct bt_mem *btmem = &btDesc->btmem; |
| bool ackReqd = false; |
| struct rproc *rproc = platform_get_drvdata(btDesc->rproc_pdev); |
| |
| if (!atomic_read(&rproc->power)) |
| return; |
| |
| spin_lock(&btDesc->lock); |
| |
| if (unlikely(!atomic_read(&btDesc->state))) { |
| btmem->rx_ctxt = |
| (struct context_info *)(btmem->virt + RX_CTXT_OFFSET); |
| } |
| else |
| bt_ipc_process_ack(btDesc); |
| |
| for (rinfo = &(btmem->rx_ctxt->sring_buf_info); rinfo != NULL; |
| rinfo = (struct ring_buffer_info *)(uintptr_t)(rinfo->next)) { |
| if (bt_ipc_process_peer_msgs(btDesc, rinfo, |
| &btmem->rx_ctxt->smsg_buf_cnt)) { |
| ackReqd = true; |
| } |
| } |
| |
| if (ackReqd) { |
| regmap_write(ipc->regmap, ipc->offset, BIT(ipc->bit)); |
| } |
| |
| if (btDesc->debug_en) |
| bt_ipc_cust_msg(btDesc, IPC_CMD_SWITCH_TO_UART); |
| |
| spin_unlock(&btDesc->lock); |
| enable_irq(ipc->irq); |
| } |
| |
| static irqreturn_t bt_ipc_irq_handler(int irq, void *data) |
| { |
| struct bt_descriptor *btDesc = data; |
| |
| disable_irq_nosync(btDesc->ipc.irq); |
| queue_work(btDesc->ipc.wq, &btDesc->ipc.work); |
| |
| return IRQ_HANDLED; |
| } |
| |
| |
| static |
| int ipc_panic_handler(struct notifier_block *nb, unsigned long event, void *ptr) |
| { |
| struct bt_descriptor *btDesc = container_of(nb, struct bt_descriptor, |
| panic_nb); |
| bt_ipc_cust_msg(btDesc, IPC_CMD_PREPARE_DUMP); |
| |
| return NOTIFY_DONE; |
| } |
| |
| int bt_ipc_init(struct bt_descriptor *btDesc) |
| { |
| int ret; |
| struct bt_ipc *ipc = &btDesc->ipc; |
| struct device *dev = &btDesc->pdev->dev; |
| |
| init_waitqueue_head(&ipc->wait_q); |
| spin_lock_init(&btDesc->lock); |
| INIT_LIST_HEAD(&ipc->tx_q); |
| |
| ipc->wq = create_singlethread_workqueue("bt_ipc"); |
| if (!ipc->wq) { |
| dev_err(dev, "failed to initialize WQ\n"); |
| return -EAGAIN; |
| } |
| |
| INIT_WORK(&ipc->work, bt_ipc_worker); |
| |
| irq_set_irqchip_state(ipc->irq, IRQCHIP_STATE_PENDING, 0); |
| irq_set_status_flags(ipc->irq, IRQ_NOAUTOEN); |
| |
| ret = devm_request_threaded_irq(dev, ipc->irq, NULL, bt_ipc_irq_handler, |
| IRQF_TRIGGER_RISING | IRQF_ONESHOT, "bt_ipc_irq", btDesc); |
| |
| if (ret) { |
| dev_err(dev, "error registering irq[%d] ret = %d\n", |
| ipc->irq, ret); |
| goto irq_err; |
| } |
| |
| btDesc->panic_nb.notifier_call = ipc_panic_handler; |
| |
| ret = atomic_notifier_chain_register(&panic_notifier_list, |
| &btDesc->panic_nb); |
| if (ret) |
| goto panic_nb_err; |
| |
| btDesc->sendmsg_cb = bt_ipc_sendmsg; |
| btDesc->sendmsg_excb = bt_ipc_sendmsg_ex; |
| |
| return 0; |
| |
| panic_nb_err: |
| devm_free_irq(dev, ipc->irq, btDesc); |
| irq_err: |
| return ret; |
| } |
| EXPORT_SYMBOL(bt_ipc_init); |
| |
| void bt_ipc_deinit(struct bt_descriptor *btDesc) |
| { |
| struct bt_ipc *ipc = &btDesc->ipc; |
| struct device *dev = &btDesc->pdev->dev; |
| |
| devm_free_irq(dev, ipc->irq, btDesc); |
| bt_ipc_purge_tx_queue(btDesc); |
| atomic_notifier_chain_unregister(&panic_notifier_list, |
| &btDesc->panic_nb); |
| flush_work(&ipc->work); |
| destroy_workqueue(ipc->wq); |
| atomic_set(&btDesc->state, 0); |
| } |
| EXPORT_SYMBOL(bt_ipc_deinit); |