Commit | Line | Data |
---|---|---|
ea7a1f27 | 1 | /* |
6be2b3d0 | 2 | * Copyright (c) 2016, Linaro Ltd. |
ea7a1f27 BA |
3 | * Copyright (c) 2015, Sony Mobile Communications Inc. |
4 | * | |
5 | * This program is free software; you can redistribute it and/or modify | |
6 | * it under the terms of the GNU General Public License version 2 and | |
7 | * only version 2 as published by the Free Software Foundation. | |
8 | * | |
9 | * This program is distributed in the hope that it will be useful, | |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | * GNU General Public License for more details. | |
13 | */ | |
14 | #include <linux/firmware.h> | |
15 | #include <linux/module.h> | |
16 | #include <linux/slab.h> | |
17 | #include <linux/soc/qcom/smd.h> | |
6be2b3d0 BA |
18 | #include <linux/io.h> |
19 | #include <linux/of_platform.h> | |
20 | #include <linux/platform_device.h> | |
21 | #include <linux/soc/qcom/wcnss_ctrl.h> | |
ea7a1f27 BA |
22 | |
23 | #define WCNSS_REQUEST_TIMEOUT (5 * HZ) | |
6be2b3d0 BA |
24 | #define WCNSS_CBC_TIMEOUT (10 * HZ) |
25 | ||
26 | #define WCNSS_ACK_DONE_BOOTING 1 | |
27 | #define WCNSS_ACK_COLD_BOOTING 2 | |
ea7a1f27 BA |
28 | |
29 | #define NV_FRAGMENT_SIZE 3072 | |
30 | #define NVBIN_FILE "wlan/prima/WCNSS_qcom_wlan_nv.bin" | |
31 | ||
32 | /** | |
33 | * struct wcnss_ctrl - driver context | |
34 | * @dev: device handle | |
35 | * @channel: SMD channel handle | |
36 | * @ack: completion for outstanding requests | |
6be2b3d0 | 37 | * @cbc: completion for cbc complete indication |
ea7a1f27 | 38 | * @ack_status: status of the outstanding request |
6be2b3d0 | 39 | * @probe_work: worker for uploading nv binary |
ea7a1f27 BA |
40 | */ |
41 | struct wcnss_ctrl { | |
42 | struct device *dev; | |
43 | struct qcom_smd_channel *channel; | |
44 | ||
45 | struct completion ack; | |
6be2b3d0 | 46 | struct completion cbc; |
ea7a1f27 BA |
47 | int ack_status; |
48 | ||
6be2b3d0 | 49 | struct work_struct probe_work; |
ea7a1f27 BA |
50 | }; |
51 | ||
52 | /* message types */ | |
53 | enum { | |
54 | WCNSS_VERSION_REQ = 0x01000000, | |
55 | WCNSS_VERSION_RESP, | |
56 | WCNSS_DOWNLOAD_NV_REQ, | |
57 | WCNSS_DOWNLOAD_NV_RESP, | |
58 | WCNSS_UPLOAD_CAL_REQ, | |
59 | WCNSS_UPLOAD_CAL_RESP, | |
60 | WCNSS_DOWNLOAD_CAL_REQ, | |
61 | WCNSS_DOWNLOAD_CAL_RESP, | |
6be2b3d0 BA |
62 | WCNSS_VBAT_LEVEL_IND, |
63 | WCNSS_BUILD_VERSION_REQ, | |
64 | WCNSS_BUILD_VERSION_RESP, | |
65 | WCNSS_PM_CONFIG_REQ, | |
66 | WCNSS_CBC_COMPLETE_IND, | |
ea7a1f27 BA |
67 | }; |
68 | ||
69 | /** | |
70 | * struct wcnss_msg_hdr - common packet header for requests and responses | |
71 | * @type: packet message type | |
72 | * @len: total length of the packet, including this header | |
73 | */ | |
74 | struct wcnss_msg_hdr { | |
75 | u32 type; | |
76 | u32 len; | |
77 | } __packed; | |
78 | ||
79 | /** | |
80 | * struct wcnss_version_resp - version request response | |
81 | * @hdr: common packet wcnss_msg_hdr header | |
82 | */ | |
83 | struct wcnss_version_resp { | |
84 | struct wcnss_msg_hdr hdr; | |
85 | u8 major; | |
86 | u8 minor; | |
87 | u8 version; | |
88 | u8 revision; | |
89 | } __packed; | |
90 | ||
91 | /** | |
92 | * struct wcnss_download_nv_req - firmware fragment request | |
93 | * @hdr: common packet wcnss_msg_hdr header | |
94 | * @seq: sequence number of this fragment | |
95 | * @last: boolean indicator of this being the last fragment of the binary | |
96 | * @frag_size: length of this fragment | |
97 | * @fragment: fragment data | |
98 | */ | |
99 | struct wcnss_download_nv_req { | |
100 | struct wcnss_msg_hdr hdr; | |
101 | u16 seq; | |
102 | u16 last; | |
103 | u32 frag_size; | |
104 | u8 fragment[]; | |
105 | } __packed; | |
106 | ||
107 | /** | |
108 | * struct wcnss_download_nv_resp - firmware download response | |
109 | * @hdr: common packet wcnss_msg_hdr header | |
110 | * @status: boolean to indicate success of the download | |
111 | */ | |
112 | struct wcnss_download_nv_resp { | |
113 | struct wcnss_msg_hdr hdr; | |
114 | u8 status; | |
115 | } __packed; | |
116 | ||
117 | /** | |
118 | * wcnss_ctrl_smd_callback() - handler from SMD responses | |
b853cb96 | 119 | * @channel: smd channel handle |
ea7a1f27 BA |
120 | * @data: pointer to the incoming data packet |
121 | * @count: size of the incoming data packet | |
122 | * | |
123 | * Handles any incoming packets from the remote WCNSS_CTRL service. | |
124 | */ | |
b853cb96 | 125 | static int wcnss_ctrl_smd_callback(struct qcom_smd_channel *channel, |
ea7a1f27 BA |
126 | const void *data, |
127 | size_t count) | |
128 | { | |
b853cb96 | 129 | struct wcnss_ctrl *wcnss = qcom_smd_get_drvdata(channel); |
ea7a1f27 BA |
130 | const struct wcnss_download_nv_resp *nvresp; |
131 | const struct wcnss_version_resp *version; | |
132 | const struct wcnss_msg_hdr *hdr = data; | |
133 | ||
134 | switch (hdr->type) { | |
135 | case WCNSS_VERSION_RESP: | |
136 | if (count != sizeof(*version)) { | |
137 | dev_err(wcnss->dev, | |
138 | "invalid size of version response\n"); | |
139 | break; | |
140 | } | |
141 | ||
142 | version = data; | |
143 | dev_info(wcnss->dev, "WCNSS Version %d.%d %d.%d\n", | |
144 | version->major, version->minor, | |
145 | version->version, version->revision); | |
146 | ||
6be2b3d0 | 147 | complete(&wcnss->ack); |
ea7a1f27 BA |
148 | break; |
149 | case WCNSS_DOWNLOAD_NV_RESP: | |
150 | if (count != sizeof(*nvresp)) { | |
151 | dev_err(wcnss->dev, | |
152 | "invalid size of download response\n"); | |
153 | break; | |
154 | } | |
155 | ||
156 | nvresp = data; | |
157 | wcnss->ack_status = nvresp->status; | |
158 | complete(&wcnss->ack); | |
159 | break; | |
6be2b3d0 BA |
160 | case WCNSS_CBC_COMPLETE_IND: |
161 | dev_dbg(wcnss->dev, "cold boot complete\n"); | |
162 | complete(&wcnss->cbc); | |
163 | break; | |
ea7a1f27 BA |
164 | default: |
165 | dev_info(wcnss->dev, "unknown message type %d\n", hdr->type); | |
166 | break; | |
167 | } | |
168 | ||
169 | return 0; | |
170 | } | |
171 | ||
172 | /** | |
173 | * wcnss_request_version() - send a version request to WCNSS | |
174 | * @wcnss: wcnss ctrl driver context | |
175 | */ | |
176 | static int wcnss_request_version(struct wcnss_ctrl *wcnss) | |
177 | { | |
178 | struct wcnss_msg_hdr msg; | |
6be2b3d0 | 179 | int ret; |
ea7a1f27 BA |
180 | |
181 | msg.type = WCNSS_VERSION_REQ; | |
182 | msg.len = sizeof(msg); | |
6be2b3d0 BA |
183 | ret = qcom_smd_send(wcnss->channel, &msg, sizeof(msg)); |
184 | if (ret < 0) | |
185 | return ret; | |
186 | ||
187 | ret = wait_for_completion_timeout(&wcnss->ack, WCNSS_CBC_TIMEOUT); | |
188 | if (!ret) { | |
189 | dev_err(wcnss->dev, "timeout waiting for version response\n"); | |
190 | return -ETIMEDOUT; | |
191 | } | |
ea7a1f27 | 192 | |
6be2b3d0 | 193 | return 0; |
ea7a1f27 BA |
194 | } |
195 | ||
196 | /** | |
197 | * wcnss_download_nv() - send nv binary to WCNSS | |
6be2b3d0 BA |
198 | * @wcnss: wcnss_ctrl state handle |
199 | * @expect_cbc: indicator to caller that an cbc event is expected | |
200 | * | |
201 | * Returns 0 on success. Negative errno on failure. | |
ea7a1f27 | 202 | */ |
6be2b3d0 | 203 | static int wcnss_download_nv(struct wcnss_ctrl *wcnss, bool *expect_cbc) |
ea7a1f27 | 204 | { |
ea7a1f27 BA |
205 | struct wcnss_download_nv_req *req; |
206 | const struct firmware *fw; | |
207 | const void *data; | |
208 | ssize_t left; | |
209 | int ret; | |
210 | ||
211 | req = kzalloc(sizeof(*req) + NV_FRAGMENT_SIZE, GFP_KERNEL); | |
212 | if (!req) | |
6be2b3d0 | 213 | return -ENOMEM; |
ea7a1f27 BA |
214 | |
215 | ret = request_firmware(&fw, NVBIN_FILE, wcnss->dev); | |
6be2b3d0 | 216 | if (ret < 0) { |
ea7a1f27 BA |
217 | dev_err(wcnss->dev, "Failed to load nv file %s: %d\n", |
218 | NVBIN_FILE, ret); | |
219 | goto free_req; | |
220 | } | |
221 | ||
222 | data = fw->data; | |
223 | left = fw->size; | |
224 | ||
225 | req->hdr.type = WCNSS_DOWNLOAD_NV_REQ; | |
226 | req->hdr.len = sizeof(*req) + NV_FRAGMENT_SIZE; | |
227 | ||
228 | req->last = 0; | |
229 | req->frag_size = NV_FRAGMENT_SIZE; | |
230 | ||
231 | req->seq = 0; | |
232 | do { | |
233 | if (left <= NV_FRAGMENT_SIZE) { | |
234 | req->last = 1; | |
235 | req->frag_size = left; | |
236 | req->hdr.len = sizeof(*req) + left; | |
237 | } | |
238 | ||
239 | memcpy(req->fragment, data, req->frag_size); | |
240 | ||
241 | ret = qcom_smd_send(wcnss->channel, req, req->hdr.len); | |
6be2b3d0 | 242 | if (ret < 0) { |
ea7a1f27 BA |
243 | dev_err(wcnss->dev, "failed to send smd packet\n"); |
244 | goto release_fw; | |
245 | } | |
246 | ||
247 | /* Increment for next fragment */ | |
248 | req->seq++; | |
249 | ||
250 | data += req->hdr.len; | |
251 | left -= NV_FRAGMENT_SIZE; | |
252 | } while (left > 0); | |
253 | ||
254 | ret = wait_for_completion_timeout(&wcnss->ack, WCNSS_REQUEST_TIMEOUT); | |
6be2b3d0 | 255 | if (!ret) { |
ea7a1f27 | 256 | dev_err(wcnss->dev, "timeout waiting for nv upload ack\n"); |
6be2b3d0 BA |
257 | ret = -ETIMEDOUT; |
258 | } else { | |
259 | *expect_cbc = wcnss->ack_status == WCNSS_ACK_COLD_BOOTING; | |
260 | ret = 0; | |
261 | } | |
ea7a1f27 BA |
262 | |
263 | release_fw: | |
264 | release_firmware(fw); | |
265 | free_req: | |
266 | kfree(req); | |
6be2b3d0 BA |
267 | |
268 | return ret; | |
269 | } | |
270 | ||
271 | /** | |
272 | * qcom_wcnss_open_channel() - open additional SMD channel to WCNSS | |
273 | * @wcnss: wcnss handle, retrieved from drvdata | |
274 | * @name: SMD channel name | |
275 | * @cb: callback to handle incoming data on the channel | |
276 | */ | |
277 | struct qcom_smd_channel *qcom_wcnss_open_channel(void *wcnss, const char *name, qcom_smd_cb_t cb) | |
278 | { | |
279 | struct wcnss_ctrl *_wcnss = wcnss; | |
280 | ||
281 | return qcom_smd_open_channel(_wcnss->channel, name, cb); | |
282 | } | |
283 | EXPORT_SYMBOL(qcom_wcnss_open_channel); | |
284 | ||
285 | static void wcnss_async_probe(struct work_struct *work) | |
286 | { | |
287 | struct wcnss_ctrl *wcnss = container_of(work, struct wcnss_ctrl, probe_work); | |
288 | bool expect_cbc; | |
289 | int ret; | |
290 | ||
291 | ret = wcnss_request_version(wcnss); | |
292 | if (ret < 0) | |
293 | return; | |
294 | ||
295 | ret = wcnss_download_nv(wcnss, &expect_cbc); | |
296 | if (ret < 0) | |
297 | return; | |
298 | ||
299 | /* Wait for pending cold boot completion if indicated by the nv downloader */ | |
300 | if (expect_cbc) { | |
301 | ret = wait_for_completion_timeout(&wcnss->cbc, WCNSS_REQUEST_TIMEOUT); | |
302 | if (!ret) | |
303 | dev_err(wcnss->dev, "expected cold boot completion\n"); | |
304 | } | |
305 | ||
306 | of_platform_populate(wcnss->dev->of_node, NULL, NULL, wcnss->dev); | |
ea7a1f27 BA |
307 | } |
308 | ||
309 | static int wcnss_ctrl_probe(struct qcom_smd_device *sdev) | |
310 | { | |
311 | struct wcnss_ctrl *wcnss; | |
312 | ||
313 | wcnss = devm_kzalloc(&sdev->dev, sizeof(*wcnss), GFP_KERNEL); | |
314 | if (!wcnss) | |
315 | return -ENOMEM; | |
316 | ||
317 | wcnss->dev = &sdev->dev; | |
318 | wcnss->channel = sdev->channel; | |
319 | ||
320 | init_completion(&wcnss->ack); | |
6be2b3d0 BA |
321 | init_completion(&wcnss->cbc); |
322 | INIT_WORK(&wcnss->probe_work, wcnss_async_probe); | |
ea7a1f27 | 323 | |
b853cb96 | 324 | qcom_smd_set_drvdata(sdev->channel, wcnss); |
6be2b3d0 BA |
325 | dev_set_drvdata(&sdev->dev, wcnss); |
326 | ||
327 | schedule_work(&wcnss->probe_work); | |
328 | ||
329 | return 0; | |
330 | } | |
331 | ||
332 | static void wcnss_ctrl_remove(struct qcom_smd_device *sdev) | |
333 | { | |
334 | struct wcnss_ctrl *wcnss = qcom_smd_get_drvdata(sdev->channel); | |
ea7a1f27 | 335 | |
6be2b3d0 BA |
336 | cancel_work_sync(&wcnss->probe_work); |
337 | of_platform_depopulate(&sdev->dev); | |
ea7a1f27 BA |
338 | } |
339 | ||
6be2b3d0 BA |
340 | static const struct of_device_id wcnss_ctrl_of_match[] = { |
341 | { .compatible = "qcom,wcnss", }, | |
ea7a1f27 BA |
342 | {} |
343 | }; | |
344 | ||
345 | static struct qcom_smd_driver wcnss_ctrl_driver = { | |
346 | .probe = wcnss_ctrl_probe, | |
6be2b3d0 | 347 | .remove = wcnss_ctrl_remove, |
ea7a1f27 | 348 | .callback = wcnss_ctrl_smd_callback, |
ea7a1f27 BA |
349 | .driver = { |
350 | .name = "qcom_wcnss_ctrl", | |
351 | .owner = THIS_MODULE, | |
6be2b3d0 | 352 | .of_match_table = wcnss_ctrl_of_match, |
ea7a1f27 BA |
353 | }, |
354 | }; | |
355 | ||
356 | module_qcom_smd_driver(wcnss_ctrl_driver); | |
357 | ||
358 | MODULE_DESCRIPTION("Qualcomm WCNSS control driver"); | |
359 | MODULE_LICENSE("GPL v2"); |