| /* |
| * Copyright (c) 2016, Linaro Ltd. |
| * Copyright (c) 2015, Sony Mobile Communications Inc. |
| * |
| * 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/firmware.h> |
| #include <linux/module.h> |
| #include <linux/slab.h> |
| #include <linux/soc/qcom/smd.h> |
| #include <linux/io.h> |
| #include <linux/of_platform.h> |
| #include <linux/platform_device.h> |
| #include <linux/soc/qcom/wcnss_ctrl.h> |
| |
| #define WCNSS_REQUEST_TIMEOUT (5 * HZ) |
| #define WCNSS_CBC_TIMEOUT (10 * HZ) |
| |
| #define WCNSS_ACK_DONE_BOOTING 1 |
| #define WCNSS_ACK_COLD_BOOTING 2 |
| |
| #define NV_FRAGMENT_SIZE 3072 |
| #define NVBIN_FILE "wlan/prima/WCNSS_qcom_wlan_nv.bin" |
| |
| /** |
| * struct wcnss_ctrl - driver context |
| * @dev: device handle |
| * @channel: SMD channel handle |
| * @ack: completion for outstanding requests |
| * @cbc: completion for cbc complete indication |
| * @ack_status: status of the outstanding request |
| * @probe_work: worker for uploading nv binary |
| */ |
| struct wcnss_ctrl { |
| struct device *dev; |
| struct qcom_smd_channel *channel; |
| |
| struct completion ack; |
| struct completion cbc; |
| int ack_status; |
| |
| struct work_struct probe_work; |
| }; |
| |
| /* message types */ |
| enum { |
| WCNSS_VERSION_REQ = 0x01000000, |
| WCNSS_VERSION_RESP, |
| WCNSS_DOWNLOAD_NV_REQ, |
| WCNSS_DOWNLOAD_NV_RESP, |
| WCNSS_UPLOAD_CAL_REQ, |
| WCNSS_UPLOAD_CAL_RESP, |
| WCNSS_DOWNLOAD_CAL_REQ, |
| WCNSS_DOWNLOAD_CAL_RESP, |
| WCNSS_VBAT_LEVEL_IND, |
| WCNSS_BUILD_VERSION_REQ, |
| WCNSS_BUILD_VERSION_RESP, |
| WCNSS_PM_CONFIG_REQ, |
| WCNSS_CBC_COMPLETE_IND, |
| }; |
| |
| /** |
| * struct wcnss_msg_hdr - common packet header for requests and responses |
| * @type: packet message type |
| * @len: total length of the packet, including this header |
| */ |
| struct wcnss_msg_hdr { |
| u32 type; |
| u32 len; |
| } __packed; |
| |
| /** |
| * struct wcnss_version_resp - version request response |
| * @hdr: common packet wcnss_msg_hdr header |
| */ |
| struct wcnss_version_resp { |
| struct wcnss_msg_hdr hdr; |
| u8 major; |
| u8 minor; |
| u8 version; |
| u8 revision; |
| } __packed; |
| |
| /** |
| * struct wcnss_download_nv_req - firmware fragment request |
| * @hdr: common packet wcnss_msg_hdr header |
| * @seq: sequence number of this fragment |
| * @last: boolean indicator of this being the last fragment of the binary |
| * @frag_size: length of this fragment |
| * @fragment: fragment data |
| */ |
| struct wcnss_download_nv_req { |
| struct wcnss_msg_hdr hdr; |
| u16 seq; |
| u16 last; |
| u32 frag_size; |
| u8 fragment[]; |
| } __packed; |
| |
| /** |
| * struct wcnss_download_nv_resp - firmware download response |
| * @hdr: common packet wcnss_msg_hdr header |
| * @status: boolean to indicate success of the download |
| */ |
| struct wcnss_download_nv_resp { |
| struct wcnss_msg_hdr hdr; |
| u8 status; |
| } __packed; |
| |
| /** |
| * wcnss_ctrl_smd_callback() - handler from SMD responses |
| * @channel: smd channel handle |
| * @data: pointer to the incoming data packet |
| * @count: size of the incoming data packet |
| * |
| * Handles any incoming packets from the remote WCNSS_CTRL service. |
| */ |
| static int wcnss_ctrl_smd_callback(struct qcom_smd_channel *channel, |
| const void *data, |
| size_t count) |
| { |
| struct wcnss_ctrl *wcnss = qcom_smd_get_drvdata(channel); |
| const struct wcnss_download_nv_resp *nvresp; |
| const struct wcnss_version_resp *version; |
| const struct wcnss_msg_hdr *hdr = data; |
| |
| switch (hdr->type) { |
| case WCNSS_VERSION_RESP: |
| if (count != sizeof(*version)) { |
| dev_err(wcnss->dev, |
| "invalid size of version response\n"); |
| break; |
| } |
| |
| version = data; |
| dev_info(wcnss->dev, "WCNSS Version %d.%d %d.%d\n", |
| version->major, version->minor, |
| version->version, version->revision); |
| |
| complete(&wcnss->ack); |
| break; |
| case WCNSS_DOWNLOAD_NV_RESP: |
| if (count != sizeof(*nvresp)) { |
| dev_err(wcnss->dev, |
| "invalid size of download response\n"); |
| break; |
| } |
| |
| nvresp = data; |
| wcnss->ack_status = nvresp->status; |
| complete(&wcnss->ack); |
| break; |
| case WCNSS_CBC_COMPLETE_IND: |
| dev_dbg(wcnss->dev, "cold boot complete\n"); |
| complete(&wcnss->cbc); |
| break; |
| default: |
| dev_info(wcnss->dev, "unknown message type %d\n", hdr->type); |
| break; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * wcnss_request_version() - send a version request to WCNSS |
| * @wcnss: wcnss ctrl driver context |
| */ |
| static int wcnss_request_version(struct wcnss_ctrl *wcnss) |
| { |
| struct wcnss_msg_hdr msg; |
| int ret; |
| |
| msg.type = WCNSS_VERSION_REQ; |
| msg.len = sizeof(msg); |
| ret = qcom_smd_send(wcnss->channel, &msg, sizeof(msg)); |
| if (ret < 0) |
| return ret; |
| |
| ret = wait_for_completion_timeout(&wcnss->ack, WCNSS_CBC_TIMEOUT); |
| if (!ret) { |
| dev_err(wcnss->dev, "timeout waiting for version response\n"); |
| return -ETIMEDOUT; |
| } |
| |
| return 0; |
| } |
| |
| /** |
| * wcnss_download_nv() - send nv binary to WCNSS |
| * @wcnss: wcnss_ctrl state handle |
| * @expect_cbc: indicator to caller that an cbc event is expected |
| * |
| * Returns 0 on success. Negative errno on failure. |
| */ |
| static int wcnss_download_nv(struct wcnss_ctrl *wcnss, bool *expect_cbc) |
| { |
| struct wcnss_download_nv_req *req; |
| const struct firmware *fw; |
| const void *data; |
| ssize_t left; |
| int ret; |
| |
| req = kzalloc(sizeof(*req) + NV_FRAGMENT_SIZE, GFP_KERNEL); |
| if (!req) |
| return -ENOMEM; |
| |
| ret = request_firmware(&fw, NVBIN_FILE, wcnss->dev); |
| if (ret < 0) { |
| dev_err(wcnss->dev, "Failed to load nv file %s: %d\n", |
| NVBIN_FILE, ret); |
| goto free_req; |
| } |
| |
| data = fw->data; |
| left = fw->size; |
| |
| req->hdr.type = WCNSS_DOWNLOAD_NV_REQ; |
| req->hdr.len = sizeof(*req) + NV_FRAGMENT_SIZE; |
| |
| req->last = 0; |
| req->frag_size = NV_FRAGMENT_SIZE; |
| |
| req->seq = 0; |
| do { |
| if (left <= NV_FRAGMENT_SIZE) { |
| req->last = 1; |
| req->frag_size = left; |
| req->hdr.len = sizeof(*req) + left; |
| } |
| |
| memcpy(req->fragment, data, req->frag_size); |
| |
| ret = qcom_smd_send(wcnss->channel, req, req->hdr.len); |
| if (ret < 0) { |
| dev_err(wcnss->dev, "failed to send smd packet\n"); |
| goto release_fw; |
| } |
| |
| /* Increment for next fragment */ |
| req->seq++; |
| |
| data += req->hdr.len; |
| left -= NV_FRAGMENT_SIZE; |
| } while (left > 0); |
| |
| ret = wait_for_completion_timeout(&wcnss->ack, WCNSS_REQUEST_TIMEOUT); |
| if (!ret) { |
| dev_err(wcnss->dev, "timeout waiting for nv upload ack\n"); |
| ret = -ETIMEDOUT; |
| } else { |
| *expect_cbc = wcnss->ack_status == WCNSS_ACK_COLD_BOOTING; |
| ret = 0; |
| } |
| |
| release_fw: |
| release_firmware(fw); |
| free_req: |
| kfree(req); |
| |
| return ret; |
| } |
| |
| /** |
| * qcom_wcnss_open_channel() - open additional SMD channel to WCNSS |
| * @wcnss: wcnss handle, retrieved from drvdata |
| * @name: SMD channel name |
| * @cb: callback to handle incoming data on the channel |
| */ |
| struct qcom_smd_channel *qcom_wcnss_open_channel(void *wcnss, const char *name, qcom_smd_cb_t cb) |
| { |
| struct wcnss_ctrl *_wcnss = wcnss; |
| |
| return qcom_smd_open_channel(_wcnss->channel, name, cb); |
| } |
| EXPORT_SYMBOL(qcom_wcnss_open_channel); |
| |
| static void wcnss_async_probe(struct work_struct *work) |
| { |
| struct wcnss_ctrl *wcnss = container_of(work, struct wcnss_ctrl, probe_work); |
| bool expect_cbc; |
| int ret; |
| |
| ret = wcnss_request_version(wcnss); |
| if (ret < 0) |
| return; |
| |
| ret = wcnss_download_nv(wcnss, &expect_cbc); |
| if (ret < 0) |
| return; |
| |
| /* Wait for pending cold boot completion if indicated by the nv downloader */ |
| if (expect_cbc) { |
| ret = wait_for_completion_timeout(&wcnss->cbc, WCNSS_REQUEST_TIMEOUT); |
| if (!ret) |
| dev_err(wcnss->dev, "expected cold boot completion\n"); |
| } |
| |
| of_platform_populate(wcnss->dev->of_node, NULL, NULL, wcnss->dev); |
| } |
| |
| static int wcnss_ctrl_probe(struct qcom_smd_device *sdev) |
| { |
| struct wcnss_ctrl *wcnss; |
| |
| wcnss = devm_kzalloc(&sdev->dev, sizeof(*wcnss), GFP_KERNEL); |
| if (!wcnss) |
| return -ENOMEM; |
| |
| wcnss->dev = &sdev->dev; |
| wcnss->channel = sdev->channel; |
| |
| init_completion(&wcnss->ack); |
| init_completion(&wcnss->cbc); |
| INIT_WORK(&wcnss->probe_work, wcnss_async_probe); |
| |
| qcom_smd_set_drvdata(sdev->channel, wcnss); |
| dev_set_drvdata(&sdev->dev, wcnss); |
| |
| schedule_work(&wcnss->probe_work); |
| |
| return 0; |
| } |
| |
| static void wcnss_ctrl_remove(struct qcom_smd_device *sdev) |
| { |
| struct wcnss_ctrl *wcnss = qcom_smd_get_drvdata(sdev->channel); |
| |
| cancel_work_sync(&wcnss->probe_work); |
| of_platform_depopulate(&sdev->dev); |
| } |
| |
| static const struct of_device_id wcnss_ctrl_of_match[] = { |
| { .compatible = "qcom,wcnss", }, |
| {} |
| }; |
| |
| static struct qcom_smd_driver wcnss_ctrl_driver = { |
| .probe = wcnss_ctrl_probe, |
| .remove = wcnss_ctrl_remove, |
| .callback = wcnss_ctrl_smd_callback, |
| .driver = { |
| .name = "qcom_wcnss_ctrl", |
| .owner = THIS_MODULE, |
| .of_match_table = wcnss_ctrl_of_match, |
| }, |
| }; |
| |
| module_qcom_smd_driver(wcnss_ctrl_driver); |
| |
| MODULE_DESCRIPTION("Qualcomm WCNSS control driver"); |
| MODULE_LICENSE("GPL v2"); |