mirror of
https://github.com/torvalds/linux.git
synced 2024-12-11 21:52:04 +00:00
90c29ed762
hdr.len includes both the size of the header and the fragment, so using
this when stepping through the firmware causes us to skip 16 bytes every
chunk of 3072 bytes; causing only the first fragment to actually be
valid data.
Instead use fragment size steps through the firmware blob.
Fixes: ea7a1f275c
("soc: qcom: Introduce WCNSS_CTRL SMD client")
Reported-by: Will Newton <will.newton@gmail.com>
Signed-off-by: Bjorn Andersson <bjorn.andersson@linaro.org>
Signed-off-by: Andy Gross <andy.gross@linaro.org>
367 lines
8.7 KiB
C
367 lines
8.7 KiB
C
/*
|
|
* 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/io.h>
|
|
#include <linux/of_platform.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/rpmsg.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 rpmsg_endpoint *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 rpmsg_device *rpdev,
|
|
void *data,
|
|
int count,
|
|
void *priv,
|
|
u32 addr)
|
|
{
|
|
struct wcnss_ctrl *wcnss = dev_get_drvdata(&rpdev->dev);
|
|
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 = rpmsg_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 = rpmsg_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 += NV_FRAGMENT_SIZE;
|
|
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 rpmsg_endpoint *qcom_wcnss_open_channel(void *wcnss, const char *name, rpmsg_rx_cb_t cb, void *priv)
|
|
{
|
|
struct rpmsg_channel_info chinfo;
|
|
struct wcnss_ctrl *_wcnss = wcnss;
|
|
|
|
strncpy(chinfo.name, name, sizeof(chinfo.name));
|
|
chinfo.src = RPMSG_ADDR_ANY;
|
|
chinfo.dst = RPMSG_ADDR_ANY;
|
|
|
|
return rpmsg_create_ept(_wcnss->channel->rpdev, cb, priv, chinfo);
|
|
}
|
|
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 rpmsg_device *rpdev)
|
|
{
|
|
struct wcnss_ctrl *wcnss;
|
|
|
|
wcnss = devm_kzalloc(&rpdev->dev, sizeof(*wcnss), GFP_KERNEL);
|
|
if (!wcnss)
|
|
return -ENOMEM;
|
|
|
|
wcnss->dev = &rpdev->dev;
|
|
wcnss->channel = rpdev->ept;
|
|
|
|
init_completion(&wcnss->ack);
|
|
init_completion(&wcnss->cbc);
|
|
INIT_WORK(&wcnss->probe_work, wcnss_async_probe);
|
|
|
|
dev_set_drvdata(&rpdev->dev, wcnss);
|
|
|
|
schedule_work(&wcnss->probe_work);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void wcnss_ctrl_remove(struct rpmsg_device *rpdev)
|
|
{
|
|
struct wcnss_ctrl *wcnss = dev_get_drvdata(&rpdev->dev);
|
|
|
|
cancel_work_sync(&wcnss->probe_work);
|
|
of_platform_depopulate(&rpdev->dev);
|
|
}
|
|
|
|
static const struct of_device_id wcnss_ctrl_of_match[] = {
|
|
{ .compatible = "qcom,wcnss", },
|
|
{}
|
|
};
|
|
MODULE_DEVICE_TABLE(of, wcnss_ctrl_of_match);
|
|
|
|
static struct rpmsg_driver wcnss_ctrl_driver = {
|
|
.probe = wcnss_ctrl_probe,
|
|
.remove = wcnss_ctrl_remove,
|
|
.callback = wcnss_ctrl_smd_callback,
|
|
.drv = {
|
|
.name = "qcom_wcnss_ctrl",
|
|
.owner = THIS_MODULE,
|
|
.of_match_table = wcnss_ctrl_of_match,
|
|
},
|
|
};
|
|
|
|
module_rpmsg_driver(wcnss_ctrl_driver);
|
|
|
|
MODULE_DESCRIPTION("Qualcomm WCNSS control driver");
|
|
MODULE_LICENSE("GPL v2");
|