Commit | Line | Data |
---|---|---|
f5fc0f86 LC |
1 | /* |
2 | * This file is part of wl1271 | |
3 | * | |
2f826f55 | 4 | * Copyright (C) 2008-2010 Nokia Corporation |
f5fc0f86 LC |
5 | * |
6 | * Contact: Luciano Coelho <luciano.coelho@nokia.com> | |
7 | * | |
8 | * This program is free software; you can redistribute it and/or | |
9 | * modify it under the terms of the GNU General Public License | |
10 | * version 2 as published by the Free Software Foundation. | |
11 | * | |
12 | * This program is distributed in the hope that it will be useful, but | |
13 | * WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
15 | * General Public License for more details. | |
16 | * | |
17 | * You should have received a copy of the GNU General Public License | |
18 | * along with this program; if not, write to the Free Software | |
19 | * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA | |
20 | * 02110-1301 USA | |
21 | * | |
22 | */ | |
23 | ||
5a0e3ad6 | 24 | #include <linux/slab.h> |
5ea417ae | 25 | #include <linux/wl12xx.h> |
ee40fa06 | 26 | #include <linux/export.h> |
f5fc0f86 | 27 | |
0f4e3122 | 28 | #include "debug.h" |
00d20100 | 29 | #include "acx.h" |
00d20100 SL |
30 | #include "boot.h" |
31 | #include "io.h" | |
32 | #include "event.h" | |
ae113b57 | 33 | #include "rx.h" |
80cd6610 | 34 | #include "hw_ops.h" |
f5fc0f86 | 35 | |
f5fc0f86 LC |
36 | static void wl1271_boot_set_ecpu_ctrl(struct wl1271 *wl, u32 flag) |
37 | { | |
38 | u32 cpu_ctrl; | |
39 | ||
40 | /* 10.5.0 run the firmware (I) */ | |
00782136 | 41 | cpu_ctrl = wlcore_read_reg(wl, REG_ECPU_CONTROL); |
f5fc0f86 LC |
42 | |
43 | /* 10.5.1 run the firmware (II) */ | |
44 | cpu_ctrl |= flag; | |
00782136 | 45 | wlcore_write_reg(wl, REG_ECPU_CONTROL, cpu_ctrl); |
f5fc0f86 LC |
46 | } |
47 | ||
7140df6e LC |
48 | static int wlcore_boot_parse_fw_ver(struct wl1271 *wl, |
49 | struct wl1271_static_data *static_data) | |
4b7fac77 LS |
50 | { |
51 | int ret; | |
52 | ||
7140df6e LC |
53 | strncpy(wl->chip.fw_ver_str, static_data->fw_version, |
54 | sizeof(wl->chip.fw_ver_str)); | |
55 | ||
56 | /* make sure the string is NULL-terminated */ | |
57 | wl->chip.fw_ver_str[sizeof(wl->chip.fw_ver_str) - 1] = '\0'; | |
58 | ||
4b7fac77 LS |
59 | ret = sscanf(wl->chip.fw_ver_str + 4, "%u.%u.%u.%u.%u", |
60 | &wl->chip.fw_ver[0], &wl->chip.fw_ver[1], | |
61 | &wl->chip.fw_ver[2], &wl->chip.fw_ver[3], | |
62 | &wl->chip.fw_ver[4]); | |
63 | ||
64 | if (ret != 5) { | |
65 | wl1271_warning("fw version incorrect value"); | |
66 | memset(wl->chip.fw_ver, 0, sizeof(wl->chip.fw_ver)); | |
7140df6e LC |
67 | ret = -EINVAL; |
68 | goto out; | |
4b7fac77 | 69 | } |
842f1a6c | 70 | |
80cd6610 LC |
71 | ret = wlcore_identify_fw(wl); |
72 | if (ret < 0) | |
7140df6e LC |
73 | goto out; |
74 | out: | |
75 | return ret; | |
4b7fac77 LS |
76 | } |
77 | ||
7140df6e | 78 | static int wlcore_boot_static_data(struct wl1271 *wl) |
f5fc0f86 | 79 | { |
690142e9 | 80 | struct wl1271_static_data *static_data; |
7140df6e | 81 | size_t len = sizeof(*static_data) + wl->static_data_priv_len; |
80cd6610 | 82 | int ret; |
f5fc0f86 | 83 | |
7140df6e | 84 | static_data = kmalloc(len, GFP_KERNEL); |
690142e9 | 85 | if (!static_data) { |
7140df6e LC |
86 | ret = -ENOMEM; |
87 | goto out; | |
690142e9 MG |
88 | } |
89 | ||
045b9b5f IY |
90 | ret = wlcore_read(wl, wl->cmd_box_addr, static_data, len, false); |
91 | if (ret < 0) | |
92 | goto out_free; | |
f5fc0f86 | 93 | |
7140df6e LC |
94 | ret = wlcore_boot_parse_fw_ver(wl, static_data); |
95 | if (ret < 0) | |
96 | goto out_free; | |
4b7fac77 | 97 | |
7140df6e | 98 | ret = wlcore_handle_static_data(wl, static_data); |
80cd6610 | 99 | if (ret < 0) |
7140df6e | 100 | goto out_free; |
80cd6610 | 101 | |
7140df6e LC |
102 | out_free: |
103 | kfree(static_data); | |
104 | out: | |
105 | return ret; | |
f5fc0f86 LC |
106 | } |
107 | ||
108 | static int wl1271_boot_upload_firmware_chunk(struct wl1271 *wl, void *buf, | |
109 | size_t fw_data_len, u32 dest) | |
110 | { | |
25a43d78 | 111 | struct wlcore_partition_set partition; |
f5fc0f86 | 112 | int addr, chunk_num, partition_limit; |
1fba4974 | 113 | u8 *p, *chunk; |
f5fc0f86 LC |
114 | |
115 | /* whal_FwCtrl_LoadFwImageSm() */ | |
116 | ||
117 | wl1271_debug(DEBUG_BOOT, "starting firmware upload"); | |
118 | ||
73d0a13c LC |
119 | wl1271_debug(DEBUG_BOOT, "fw_data_len %zd chunk_size %d", |
120 | fw_data_len, CHUNK_SIZE); | |
f5fc0f86 | 121 | |
f5fc0f86 LC |
122 | if ((fw_data_len % 4) != 0) { |
123 | wl1271_error("firmware length not multiple of four"); | |
124 | return -EIO; | |
125 | } | |
126 | ||
1fba4974 | 127 | chunk = kmalloc(CHUNK_SIZE, GFP_KERNEL); |
ed317788 | 128 | if (!chunk) { |
1fba4974 JO |
129 | wl1271_error("allocation for firmware upload chunk failed"); |
130 | return -ENOMEM; | |
131 | } | |
132 | ||
25a43d78 | 133 | memcpy(&partition, &wl->ptable[PART_DOWN], sizeof(partition)); |
451de97a | 134 | partition.mem.start = dest; |
25a43d78 | 135 | wlcore_set_partition(wl, &partition); |
f5fc0f86 LC |
136 | |
137 | /* 10.1 set partition limit and chunk num */ | |
138 | chunk_num = 0; | |
25a43d78 | 139 | partition_limit = wl->ptable[PART_DOWN].mem.size; |
f5fc0f86 LC |
140 | |
141 | while (chunk_num < fw_data_len / CHUNK_SIZE) { | |
142 | /* 10.2 update partition, if needed */ | |
143 | addr = dest + (chunk_num + 2) * CHUNK_SIZE; | |
144 | if (addr > partition_limit) { | |
145 | addr = dest + chunk_num * CHUNK_SIZE; | |
146 | partition_limit = chunk_num * CHUNK_SIZE + | |
25a43d78 | 147 | wl->ptable[PART_DOWN].mem.size; |
451de97a | 148 | partition.mem.start = addr; |
25a43d78 | 149 | wlcore_set_partition(wl, &partition); |
f5fc0f86 LC |
150 | } |
151 | ||
152 | /* 10.3 upload the chunk */ | |
153 | addr = dest + chunk_num * CHUNK_SIZE; | |
154 | p = buf + chunk_num * CHUNK_SIZE; | |
1fba4974 | 155 | memcpy(chunk, p, CHUNK_SIZE); |
f5fc0f86 LC |
156 | wl1271_debug(DEBUG_BOOT, "uploading fw chunk 0x%p to 0x%x", |
157 | p, addr); | |
7b048c52 | 158 | wl1271_write(wl, addr, chunk, CHUNK_SIZE, false); |
f5fc0f86 LC |
159 | |
160 | chunk_num++; | |
161 | } | |
162 | ||
163 | /* 10.4 upload the last chunk */ | |
164 | addr = dest + chunk_num * CHUNK_SIZE; | |
165 | p = buf + chunk_num * CHUNK_SIZE; | |
1fba4974 | 166 | memcpy(chunk, p, fw_data_len % CHUNK_SIZE); |
73d0a13c | 167 | wl1271_debug(DEBUG_BOOT, "uploading fw last chunk (%zd B) 0x%p to 0x%x", |
f5fc0f86 | 168 | fw_data_len % CHUNK_SIZE, p, addr); |
7b048c52 | 169 | wl1271_write(wl, addr, chunk, fw_data_len % CHUNK_SIZE, false); |
f5fc0f86 | 170 | |
1fba4974 | 171 | kfree(chunk); |
f5fc0f86 LC |
172 | return 0; |
173 | } | |
174 | ||
dd5512eb | 175 | int wlcore_boot_upload_firmware(struct wl1271 *wl) |
f5fc0f86 LC |
176 | { |
177 | u32 chunks, addr, len; | |
ed317788 | 178 | int ret = 0; |
f5fc0f86 LC |
179 | u8 *fw; |
180 | ||
181 | fw = wl->fw; | |
d0f63b20 | 182 | chunks = be32_to_cpup((__be32 *) fw); |
f5fc0f86 LC |
183 | fw += sizeof(u32); |
184 | ||
185 | wl1271_debug(DEBUG_BOOT, "firmware chunks to be uploaded: %u", chunks); | |
186 | ||
187 | while (chunks--) { | |
d0f63b20 | 188 | addr = be32_to_cpup((__be32 *) fw); |
f5fc0f86 | 189 | fw += sizeof(u32); |
d0f63b20 | 190 | len = be32_to_cpup((__be32 *) fw); |
f5fc0f86 LC |
191 | fw += sizeof(u32); |
192 | ||
193 | if (len > 300000) { | |
194 | wl1271_info("firmware chunk too long: %u", len); | |
195 | return -EINVAL; | |
196 | } | |
197 | wl1271_debug(DEBUG_BOOT, "chunk %d addr 0x%x len %u", | |
198 | chunks, addr, len); | |
ed317788 JO |
199 | ret = wl1271_boot_upload_firmware_chunk(wl, fw, len, addr); |
200 | if (ret != 0) | |
201 | break; | |
f5fc0f86 LC |
202 | fw += len; |
203 | } | |
204 | ||
ed317788 | 205 | return ret; |
f5fc0f86 | 206 | } |
dd5512eb | 207 | EXPORT_SYMBOL_GPL(wlcore_boot_upload_firmware); |
f5fc0f86 | 208 | |
dd5512eb | 209 | int wlcore_boot_upload_nvs(struct wl1271 *wl) |
f5fc0f86 LC |
210 | { |
211 | size_t nvs_len, burst_len; | |
212 | int i; | |
213 | u32 dest_addr, val; | |
152ee6e0 | 214 | u8 *nvs_ptr, *nvs_aligned; |
f5fc0f86 | 215 | |
3e3947ff AN |
216 | if (wl->nvs == NULL) { |
217 | wl1271_error("NVS file is needed during boot"); | |
f5fc0f86 | 218 | return -ENODEV; |
3e3947ff | 219 | } |
f5fc0f86 | 220 | |
d203e59c | 221 | if (wl->quirks & WLCORE_QUIRK_LEGACY_NVS) { |
bc765bf3 SL |
222 | struct wl1271_nvs_file *nvs = |
223 | (struct wl1271_nvs_file *)wl->nvs; | |
224 | /* | |
225 | * FIXME: the LEGACY NVS image support (NVS's missing the 5GHz | |
226 | * band configurations) can be removed when those NVS files stop | |
227 | * floating around. | |
228 | */ | |
229 | if (wl->nvs_len == sizeof(struct wl1271_nvs_file) || | |
230 | wl->nvs_len == WL1271_INI_LEGACY_NVS_FILE_SIZE) { | |
cabb81c9 | 231 | if (nvs->general_params.dual_mode_select) |
bc765bf3 SL |
232 | wl->enable_11a = true; |
233 | } | |
02fabb0e | 234 | |
bc765bf3 SL |
235 | if (wl->nvs_len != sizeof(struct wl1271_nvs_file) && |
236 | (wl->nvs_len != WL1271_INI_LEGACY_NVS_FILE_SIZE || | |
237 | wl->enable_11a)) { | |
238 | wl1271_error("nvs size is not as expected: %zu != %zu", | |
239 | wl->nvs_len, sizeof(struct wl1271_nvs_file)); | |
240 | kfree(wl->nvs); | |
241 | wl->nvs = NULL; | |
242 | wl->nvs_len = 0; | |
243 | return -EILSEQ; | |
244 | } | |
245 | ||
246 | /* only the first part of the NVS needs to be uploaded */ | |
247 | nvs_len = sizeof(nvs->nvs); | |
248 | nvs_ptr = (u8 *) nvs->nvs; | |
d203e59c LC |
249 | } else { |
250 | struct wl128x_nvs_file *nvs = (struct wl128x_nvs_file *)wl->nvs; | |
251 | ||
252 | if (wl->nvs_len == sizeof(struct wl128x_nvs_file)) { | |
253 | if (nvs->general_params.dual_mode_select) | |
254 | wl->enable_11a = true; | |
255 | } else { | |
256 | wl1271_error("nvs size is not as expected: %zu != %zu", | |
257 | wl->nvs_len, | |
258 | sizeof(struct wl128x_nvs_file)); | |
259 | kfree(wl->nvs); | |
260 | wl->nvs = NULL; | |
261 | wl->nvs_len = 0; | |
262 | return -EILSEQ; | |
263 | } | |
264 | ||
265 | /* only the first part of the NVS needs to be uploaded */ | |
266 | nvs_len = sizeof(nvs->nvs); | |
267 | nvs_ptr = (u8 *)nvs->nvs; | |
bc765bf3 | 268 | } |
f5fc0f86 | 269 | |
1b72aecd | 270 | /* update current MAC address to NVS */ |
5e037e74 LC |
271 | nvs_ptr[11] = wl->addresses[0].addr[0]; |
272 | nvs_ptr[10] = wl->addresses[0].addr[1]; | |
273 | nvs_ptr[6] = wl->addresses[0].addr[2]; | |
274 | nvs_ptr[5] = wl->addresses[0].addr[3]; | |
275 | nvs_ptr[4] = wl->addresses[0].addr[4]; | |
276 | nvs_ptr[3] = wl->addresses[0].addr[5]; | |
1b72aecd | 277 | |
f5fc0f86 LC |
278 | /* |
279 | * Layout before the actual NVS tables: | |
280 | * 1 byte : burst length. | |
281 | * 2 bytes: destination address. | |
282 | * n bytes: data to burst copy. | |
283 | * | |
284 | * This is ended by a 0 length, then the NVS tables. | |
285 | */ | |
286 | ||
287 | /* FIXME: Do we need to check here whether the LSB is 1? */ | |
288 | while (nvs_ptr[0]) { | |
289 | burst_len = nvs_ptr[0]; | |
290 | dest_addr = (nvs_ptr[1] & 0xfe) | ((u32)(nvs_ptr[2] << 8)); | |
291 | ||
2f63b011 JO |
292 | /* |
293 | * Due to our new wl1271_translate_reg_addr function, | |
00782136 LC |
294 | * we need to add the register partition start address |
295 | * to the destination | |
2f63b011 | 296 | */ |
00782136 | 297 | dest_addr += wl->curr_part.reg.start; |
f5fc0f86 LC |
298 | |
299 | /* We move our pointer to the data */ | |
300 | nvs_ptr += 3; | |
301 | ||
302 | for (i = 0; i < burst_len; i++) { | |
f6efe96e PF |
303 | if (nvs_ptr + 3 >= (u8 *) wl->nvs + nvs_len) |
304 | goto out_badnvs; | |
305 | ||
f5fc0f86 LC |
306 | val = (nvs_ptr[0] | (nvs_ptr[1] << 8) |
307 | | (nvs_ptr[2] << 16) | (nvs_ptr[3] << 24)); | |
308 | ||
309 | wl1271_debug(DEBUG_BOOT, | |
310 | "nvs burst write 0x%x: 0x%x", | |
311 | dest_addr, val); | |
7b048c52 | 312 | wl1271_write32(wl, dest_addr, val); |
f5fc0f86 LC |
313 | |
314 | nvs_ptr += 4; | |
315 | dest_addr += 4; | |
316 | } | |
f6efe96e PF |
317 | |
318 | if (nvs_ptr >= (u8 *) wl->nvs + nvs_len) | |
319 | goto out_badnvs; | |
f5fc0f86 LC |
320 | } |
321 | ||
322 | /* | |
323 | * We've reached the first zero length, the first NVS table | |
67e0208a | 324 | * is located at an aligned offset which is at least 7 bytes further. |
bc765bf3 SL |
325 | * NOTE: The wl->nvs->nvs element must be first, in order to |
326 | * simplify the casting, we assume it is at the beginning of | |
327 | * the wl->nvs structure. | |
f5fc0f86 | 328 | */ |
bc765bf3 SL |
329 | nvs_ptr = (u8 *)wl->nvs + |
330 | ALIGN(nvs_ptr - (u8 *)wl->nvs + 7, 4); | |
f6efe96e PF |
331 | |
332 | if (nvs_ptr >= (u8 *) wl->nvs + nvs_len) | |
333 | goto out_badnvs; | |
334 | ||
bc765bf3 | 335 | nvs_len -= nvs_ptr - (u8 *)wl->nvs; |
f5fc0f86 | 336 | |
f5fc0f86 | 337 | /* Now we must set the partition correctly */ |
25a43d78 | 338 | wlcore_set_partition(wl, &wl->ptable[PART_WORK]); |
f5fc0f86 LC |
339 | |
340 | /* Copy the NVS tables to a new block to ensure alignment */ | |
67e0208a IY |
341 | nvs_aligned = kmemdup(nvs_ptr, nvs_len, GFP_KERNEL); |
342 | if (!nvs_aligned) | |
343 | return -ENOMEM; | |
f5fc0f86 LC |
344 | |
345 | /* And finally we upload the NVS tables */ | |
00782136 LC |
346 | wlcore_write_data(wl, REG_CMD_MBOX_ADDRESS, |
347 | nvs_aligned, nvs_len, false); | |
f5fc0f86 LC |
348 | |
349 | kfree(nvs_aligned); | |
350 | return 0; | |
f6efe96e PF |
351 | |
352 | out_badnvs: | |
353 | wl1271_error("nvs data is malformed"); | |
354 | return -EILSEQ; | |
f5fc0f86 | 355 | } |
dd5512eb | 356 | EXPORT_SYMBOL_GPL(wlcore_boot_upload_nvs); |
f5fc0f86 | 357 | |
dd5512eb | 358 | int wlcore_boot_run_firmware(struct wl1271 *wl) |
f5fc0f86 LC |
359 | { |
360 | int loop, ret; | |
23a7a51c | 361 | u32 chip_id, intr; |
f5fc0f86 | 362 | |
dd5512eb LC |
363 | /* Make sure we have the boot partition */ |
364 | wlcore_set_partition(wl, &wl->ptable[PART_BOOT]); | |
365 | ||
f5fc0f86 LC |
366 | wl1271_boot_set_ecpu_ctrl(wl, ECPU_CONTROL_HALT); |
367 | ||
00782136 | 368 | chip_id = wlcore_read_reg(wl, REG_CHIP_ID_B); |
f5fc0f86 LC |
369 | |
370 | wl1271_debug(DEBUG_BOOT, "chip id after firmware boot: 0x%x", chip_id); | |
371 | ||
372 | if (chip_id != wl->chip.id) { | |
373 | wl1271_error("chip id doesn't match after firmware boot"); | |
374 | return -EIO; | |
375 | } | |
376 | ||
377 | /* wait for init to complete */ | |
378 | loop = 0; | |
379 | while (loop++ < INIT_LOOP) { | |
380 | udelay(INIT_LOOP_DELAY); | |
00782136 | 381 | intr = wlcore_read_reg(wl, REG_INTERRUPT_NO_CLEAR); |
f5fc0f86 | 382 | |
23a7a51c | 383 | if (intr == 0xffffffff) { |
f5fc0f86 LC |
384 | wl1271_error("error reading hardware complete " |
385 | "init indication"); | |
386 | return -EIO; | |
387 | } | |
388 | /* check that ACX_INTR_INIT_COMPLETE is enabled */ | |
23a7a51c | 389 | else if (intr & WL1271_ACX_INTR_INIT_COMPLETE) { |
00782136 LC |
390 | wlcore_write_reg(wl, REG_INTERRUPT_ACK, |
391 | WL1271_ACX_INTR_INIT_COMPLETE); | |
f5fc0f86 LC |
392 | break; |
393 | } | |
394 | } | |
395 | ||
e7d17cf4 | 396 | if (loop > INIT_LOOP) { |
f5fc0f86 LC |
397 | wl1271_error("timeout waiting for the hardware to " |
398 | "complete initialization"); | |
399 | return -EIO; | |
400 | } | |
401 | ||
402 | /* get hardware config command mail box */ | |
00782136 | 403 | wl->cmd_box_addr = wlcore_read_reg(wl, REG_COMMAND_MAILBOX_PTR); |
f5fc0f86 | 404 | |
4263c5f2 LC |
405 | wl1271_debug(DEBUG_MAILBOX, "cmd_box_addr 0x%x", wl->cmd_box_addr); |
406 | ||
f5fc0f86 | 407 | /* get hardware config event mail box */ |
4263c5f2 LC |
408 | wl->mbox_ptr[0] = wlcore_read_reg(wl, REG_EVENT_MAILBOX_PTR); |
409 | wl->mbox_ptr[1] = wl->mbox_ptr[0] + sizeof(struct event_mailbox); | |
f5fc0f86 | 410 | |
4263c5f2 LC |
411 | wl1271_debug(DEBUG_MAILBOX, "MBOX ptrs: 0x%x 0x%x", |
412 | wl->mbox_ptr[0], wl->mbox_ptr[1]); | |
f5fc0f86 | 413 | |
7140df6e | 414 | ret = wlcore_boot_static_data(wl); |
80cd6610 | 415 | if (ret < 0) { |
7140df6e | 416 | wl1271_error("error getting static data"); |
80cd6610 LC |
417 | return ret; |
418 | } | |
f5fc0f86 LC |
419 | |
420 | /* | |
421 | * in case of full asynchronous mode the firmware event must be | |
422 | * ready to receive event from the command mailbox | |
423 | */ | |
424 | ||
be823e5b JO |
425 | /* unmask required mbox events */ |
426 | wl->event_mask = BSS_LOSE_EVENT_ID | | |
5f561f68 | 427 | REGAINED_BSS_EVENT_ID | |
19ad0715 | 428 | SCAN_COMPLETE_EVENT_ID | |
8332f0f6 | 429 | ROLE_STOP_COMPLETE_EVENT_ID | |
90494a90 | 430 | RSSI_SNR_TRIGGER_0_EVENT_ID | |
8d2ef7bd | 431 | PSPOLL_DELIVERY_FAILURE_EVENT_ID | |
6394c01b LC |
432 | SOFT_GEMINI_SENSE_EVENT_ID | |
433 | PERIODIC_SCAN_REPORT_EVENT_ID | | |
c690ec81 EP |
434 | PERIODIC_SCAN_COMPLETE_EVENT_ID | |
435 | DUMMY_PACKET_EVENT_ID | | |
436 | PEER_REMOVE_COMPLETE_EVENT_ID | | |
437 | BA_SESSION_RX_CONSTRAINT_EVENT_ID | | |
438 | REMAIN_ON_CHANNEL_COMPLETE_EVENT_ID | | |
439 | INACTIVE_STA_EVENT_ID | | |
6d158ff3 SL |
440 | MAX_TX_RETRY_EVENT_ID | |
441 | CHANNEL_SWITCH_COMPLETE_EVENT_ID; | |
203c903c | 442 | |
f5fc0f86 LC |
443 | ret = wl1271_event_unmask(wl); |
444 | if (ret < 0) { | |
445 | wl1271_error("EVENT mask setting failed"); | |
446 | return ret; | |
447 | } | |
448 | ||
4263c5f2 LC |
449 | /* set the working partition to its "running" mode offset */ |
450 | wlcore_set_partition(wl, &wl->ptable[PART_WORK]); | |
f5fc0f86 LC |
451 | |
452 | /* firmware startup completed */ | |
453 | return 0; | |
454 | } | |
dd5512eb | 455 | EXPORT_SYMBOL_GPL(wlcore_boot_run_firmware); |