forked from Minki/linux
1110a2dbe6
The RTL8822BE is a new Realtek wifi and BT device. Support for the BT part is hereby added. As this device is similar to most of the other Realtek BT devices, the changes are minimal. The main difference is that the 8822BE needs a configuration file for enabling and disabling features. Thus code is added to select and load this configuration file. Although not needed at the moment, hooks are added for the other devices that might need such configuration files. One additional change is to the routine that tests that the project ID contained in the firmware matches the hardware. As the project IDs are not sequential, continuing to use the position in the array as the expected value of the ID would require adding extra unused entries in the table, and any subsequant rearrangment of the array would break the code. To fix these problems, the array elements now contain both the hardware ID and the expected value for the project ID. Signed-off-by: 陆朱伟 <alex_lu@realsil.com.cn> Signed-off-by: Larry Finger <Larry.Finger@lwfinger.net> Signed-off-by: Marcel Holtmann <marcel@holtmann.org>
474 lines
12 KiB
C
474 lines
12 KiB
C
/*
|
|
* Bluetooth support for Realtek devices
|
|
*
|
|
* Copyright (C) 2015 Endless Mobile, Inc.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 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/module.h>
|
|
#include <linux/firmware.h>
|
|
#include <asm/unaligned.h>
|
|
#include <linux/usb.h>
|
|
|
|
#include <net/bluetooth/bluetooth.h>
|
|
#include <net/bluetooth/hci_core.h>
|
|
|
|
#include "btrtl.h"
|
|
|
|
#define VERSION "0.1"
|
|
|
|
#define RTL_EPATCH_SIGNATURE "Realtech"
|
|
#define RTL_ROM_LMP_3499 0x3499
|
|
#define RTL_ROM_LMP_8723A 0x1200
|
|
#define RTL_ROM_LMP_8723B 0x8723
|
|
#define RTL_ROM_LMP_8821A 0x8821
|
|
#define RTL_ROM_LMP_8761A 0x8761
|
|
#define RTL_ROM_LMP_8822B 0x8822
|
|
|
|
static int rtl_read_rom_version(struct hci_dev *hdev, u8 *version)
|
|
{
|
|
struct rtl_rom_version_evt *rom_version;
|
|
struct sk_buff *skb;
|
|
|
|
/* Read RTL ROM version command */
|
|
skb = __hci_cmd_sync(hdev, 0xfc6d, 0, NULL, HCI_INIT_TIMEOUT);
|
|
if (IS_ERR(skb)) {
|
|
BT_ERR("%s: Read ROM version failed (%ld)",
|
|
hdev->name, PTR_ERR(skb));
|
|
return PTR_ERR(skb);
|
|
}
|
|
|
|
if (skb->len != sizeof(*rom_version)) {
|
|
BT_ERR("%s: RTL version event length mismatch", hdev->name);
|
|
kfree_skb(skb);
|
|
return -EIO;
|
|
}
|
|
|
|
rom_version = (struct rtl_rom_version_evt *)skb->data;
|
|
BT_INFO("%s: rom_version status=%x version=%x",
|
|
hdev->name, rom_version->status, rom_version->version);
|
|
|
|
*version = rom_version->version;
|
|
|
|
kfree_skb(skb);
|
|
return 0;
|
|
}
|
|
|
|
static int rtl8723b_parse_firmware(struct hci_dev *hdev, u16 lmp_subver,
|
|
const struct firmware *fw,
|
|
unsigned char **_buf)
|
|
{
|
|
const u8 extension_sig[] = { 0x51, 0x04, 0xfd, 0x77 };
|
|
struct rtl_epatch_header *epatch_info;
|
|
unsigned char *buf;
|
|
int i, ret, len;
|
|
size_t min_size;
|
|
u8 opcode, length, data, rom_version = 0;
|
|
int project_id = -1;
|
|
const unsigned char *fwptr, *chip_id_base;
|
|
const unsigned char *patch_length_base, *patch_offset_base;
|
|
u32 patch_offset = 0;
|
|
u16 patch_length, num_patches;
|
|
static const struct {
|
|
__u16 lmp_subver;
|
|
__u8 id;
|
|
} project_id_to_lmp_subver[] = {
|
|
{ RTL_ROM_LMP_8723A, 0 },
|
|
{ RTL_ROM_LMP_8723B, 1 },
|
|
{ RTL_ROM_LMP_8821A, 2 },
|
|
{ RTL_ROM_LMP_8761A, 3 },
|
|
{ RTL_ROM_LMP_8822B, 8 },
|
|
};
|
|
|
|
ret = rtl_read_rom_version(hdev, &rom_version);
|
|
if (ret)
|
|
return ret;
|
|
|
|
min_size = sizeof(struct rtl_epatch_header) + sizeof(extension_sig) + 3;
|
|
if (fw->size < min_size)
|
|
return -EINVAL;
|
|
|
|
fwptr = fw->data + fw->size - sizeof(extension_sig);
|
|
if (memcmp(fwptr, extension_sig, sizeof(extension_sig)) != 0) {
|
|
BT_ERR("%s: extension section signature mismatch", hdev->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Loop from the end of the firmware parsing instructions, until
|
|
* we find an instruction that identifies the "project ID" for the
|
|
* hardware supported by this firwmare file.
|
|
* Once we have that, we double-check that that project_id is suitable
|
|
* for the hardware we are working with.
|
|
*/
|
|
while (fwptr >= fw->data + (sizeof(struct rtl_epatch_header) + 3)) {
|
|
opcode = *--fwptr;
|
|
length = *--fwptr;
|
|
data = *--fwptr;
|
|
|
|
BT_DBG("check op=%x len=%x data=%x", opcode, length, data);
|
|
|
|
if (opcode == 0xff) /* EOF */
|
|
break;
|
|
|
|
if (length == 0) {
|
|
BT_ERR("%s: found instruction with length 0",
|
|
hdev->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (opcode == 0 && length == 1) {
|
|
project_id = data;
|
|
break;
|
|
}
|
|
|
|
fwptr -= length;
|
|
}
|
|
|
|
if (project_id < 0) {
|
|
BT_ERR("%s: failed to find version instruction", hdev->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Find project_id in table */
|
|
for (i = 0; i < ARRAY_SIZE(project_id_to_lmp_subver); i++) {
|
|
if (project_id == project_id_to_lmp_subver[i].id)
|
|
break;
|
|
}
|
|
|
|
if (i >= ARRAY_SIZE(project_id_to_lmp_subver)) {
|
|
BT_ERR("%s: unknown project id %d", hdev->name, project_id);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (lmp_subver != project_id_to_lmp_subver[i].lmp_subver) {
|
|
BT_ERR("%s: firmware is for %x but this is a %x", hdev->name,
|
|
project_id_to_lmp_subver[i].lmp_subver, lmp_subver);
|
|
return -EINVAL;
|
|
}
|
|
|
|
epatch_info = (struct rtl_epatch_header *)fw->data;
|
|
if (memcmp(epatch_info->signature, RTL_EPATCH_SIGNATURE, 8) != 0) {
|
|
BT_ERR("%s: bad EPATCH signature", hdev->name);
|
|
return -EINVAL;
|
|
}
|
|
|
|
num_patches = le16_to_cpu(epatch_info->num_patches);
|
|
BT_DBG("fw_version=%x, num_patches=%d",
|
|
le32_to_cpu(epatch_info->fw_version), num_patches);
|
|
|
|
/* After the rtl_epatch_header there is a funky patch metadata section.
|
|
* Assuming 2 patches, the layout is:
|
|
* ChipID1 ChipID2 PatchLength1 PatchLength2 PatchOffset1 PatchOffset2
|
|
*
|
|
* Find the right patch for this chip.
|
|
*/
|
|
min_size += 8 * num_patches;
|
|
if (fw->size < min_size)
|
|
return -EINVAL;
|
|
|
|
chip_id_base = fw->data + sizeof(struct rtl_epatch_header);
|
|
patch_length_base = chip_id_base + (sizeof(u16) * num_patches);
|
|
patch_offset_base = patch_length_base + (sizeof(u16) * num_patches);
|
|
for (i = 0; i < num_patches; i++) {
|
|
u16 chip_id = get_unaligned_le16(chip_id_base +
|
|
(i * sizeof(u16)));
|
|
if (chip_id == rom_version + 1) {
|
|
patch_length = get_unaligned_le16(patch_length_base +
|
|
(i * sizeof(u16)));
|
|
patch_offset = get_unaligned_le32(patch_offset_base +
|
|
(i * sizeof(u32)));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!patch_offset) {
|
|
BT_ERR("%s: didn't find patch for chip id %d",
|
|
hdev->name, rom_version);
|
|
return -EINVAL;
|
|
}
|
|
|
|
BT_DBG("length=%x offset=%x index %d", patch_length, patch_offset, i);
|
|
min_size = patch_offset + patch_length;
|
|
if (fw->size < min_size)
|
|
return -EINVAL;
|
|
|
|
/* Copy the firmware into a new buffer and write the version at
|
|
* the end.
|
|
*/
|
|
len = patch_length;
|
|
buf = kmemdup(fw->data + patch_offset, patch_length, GFP_KERNEL);
|
|
if (!buf)
|
|
return -ENOMEM;
|
|
|
|
memcpy(buf + patch_length - 4, &epatch_info->fw_version, 4);
|
|
|
|
*_buf = buf;
|
|
return len;
|
|
}
|
|
|
|
static int rtl_download_firmware(struct hci_dev *hdev,
|
|
const unsigned char *data, int fw_len)
|
|
{
|
|
struct rtl_download_cmd *dl_cmd;
|
|
int frag_num = fw_len / RTL_FRAG_LEN + 1;
|
|
int frag_len = RTL_FRAG_LEN;
|
|
int ret = 0;
|
|
int i;
|
|
|
|
dl_cmd = kmalloc(sizeof(struct rtl_download_cmd), GFP_KERNEL);
|
|
if (!dl_cmd)
|
|
return -ENOMEM;
|
|
|
|
for (i = 0; i < frag_num; i++) {
|
|
struct sk_buff *skb;
|
|
|
|
BT_DBG("download fw (%d/%d)", i, frag_num);
|
|
|
|
dl_cmd->index = i;
|
|
if (i == (frag_num - 1)) {
|
|
dl_cmd->index |= 0x80; /* data end */
|
|
frag_len = fw_len % RTL_FRAG_LEN;
|
|
}
|
|
memcpy(dl_cmd->data, data, frag_len);
|
|
|
|
/* Send download command */
|
|
skb = __hci_cmd_sync(hdev, 0xfc20, frag_len + 1, dl_cmd,
|
|
HCI_INIT_TIMEOUT);
|
|
if (IS_ERR(skb)) {
|
|
BT_ERR("%s: download fw command failed (%ld)",
|
|
hdev->name, PTR_ERR(skb));
|
|
ret = -PTR_ERR(skb);
|
|
goto out;
|
|
}
|
|
|
|
if (skb->len != sizeof(struct rtl_download_response)) {
|
|
BT_ERR("%s: download fw event length mismatch",
|
|
hdev->name);
|
|
kfree_skb(skb);
|
|
ret = -EIO;
|
|
goto out;
|
|
}
|
|
|
|
kfree_skb(skb);
|
|
data += RTL_FRAG_LEN;
|
|
}
|
|
|
|
out:
|
|
kfree(dl_cmd);
|
|
return ret;
|
|
}
|
|
|
|
static int rtl_load_config(struct hci_dev *hdev, const char *name, u8 **buff)
|
|
{
|
|
const struct firmware *fw;
|
|
int ret;
|
|
|
|
BT_INFO("%s: rtl: loading %s", hdev->name, name);
|
|
ret = request_firmware(&fw, name, &hdev->dev);
|
|
if (ret < 0) {
|
|
BT_ERR("%s: Failed to load %s", hdev->name, name);
|
|
return ret;
|
|
}
|
|
|
|
ret = fw->size;
|
|
*buff = kmemdup(fw->data, ret, GFP_KERNEL);
|
|
|
|
release_firmware(fw);
|
|
|
|
return ret;
|
|
}
|
|
|
|
static int btrtl_setup_rtl8723a(struct hci_dev *hdev)
|
|
{
|
|
const struct firmware *fw;
|
|
int ret;
|
|
|
|
BT_INFO("%s: rtl: loading rtl_bt/rtl8723a_fw.bin", hdev->name);
|
|
ret = request_firmware(&fw, "rtl_bt/rtl8723a_fw.bin", &hdev->dev);
|
|
if (ret < 0) {
|
|
BT_ERR("%s: Failed to load rtl_bt/rtl8723a_fw.bin", hdev->name);
|
|
return ret;
|
|
}
|
|
|
|
if (fw->size < 8) {
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
/* Check that the firmware doesn't have the epatch signature
|
|
* (which is only for RTL8723B and newer).
|
|
*/
|
|
if (!memcmp(fw->data, RTL_EPATCH_SIGNATURE, 8)) {
|
|
BT_ERR("%s: unexpected EPATCH signature!", hdev->name);
|
|
ret = -EINVAL;
|
|
goto out;
|
|
}
|
|
|
|
ret = rtl_download_firmware(hdev, fw->data, fw->size);
|
|
|
|
out:
|
|
release_firmware(fw);
|
|
return ret;
|
|
}
|
|
|
|
static int btrtl_setup_rtl8723b(struct hci_dev *hdev, u16 lmp_subver,
|
|
const char *fw_name)
|
|
{
|
|
unsigned char *fw_data = NULL;
|
|
const struct firmware *fw;
|
|
int ret;
|
|
int cfg_sz;
|
|
u8 *cfg_buff = NULL;
|
|
u8 *tbuff;
|
|
char *cfg_name = NULL;
|
|
|
|
switch (lmp_subver) {
|
|
case RTL_ROM_LMP_8723B:
|
|
cfg_name = "rtl_bt/rtl8723b_config.bin";
|
|
break;
|
|
case RTL_ROM_LMP_8821A:
|
|
cfg_name = "rtl_bt/rtl8821a_config.bin";
|
|
break;
|
|
case RTL_ROM_LMP_8761A:
|
|
cfg_name = "rtl_bt/rtl8761a_config.bin";
|
|
break;
|
|
case RTL_ROM_LMP_8822B:
|
|
cfg_name = "rtl_bt/rtl8822b_config.bin";
|
|
break;
|
|
default:
|
|
BT_ERR("%s: rtl: no config according to lmp_subver %04x",
|
|
hdev->name, lmp_subver);
|
|
break;
|
|
}
|
|
|
|
if (cfg_name) {
|
|
cfg_sz = rtl_load_config(hdev, cfg_name, &cfg_buff);
|
|
if (cfg_sz < 0)
|
|
cfg_sz = 0;
|
|
} else
|
|
cfg_sz = 0;
|
|
|
|
BT_INFO("%s: rtl: loading %s", hdev->name, fw_name);
|
|
ret = request_firmware(&fw, fw_name, &hdev->dev);
|
|
if (ret < 0) {
|
|
BT_ERR("%s: Failed to load %s", hdev->name, fw_name);
|
|
goto err_req_fw;
|
|
}
|
|
|
|
ret = rtl8723b_parse_firmware(hdev, lmp_subver, fw, &fw_data);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
if (cfg_sz) {
|
|
tbuff = kzalloc(ret + cfg_sz, GFP_KERNEL);
|
|
if (!tbuff) {
|
|
ret = -ENOMEM;
|
|
goto out;
|
|
}
|
|
|
|
memcpy(tbuff, fw_data, ret);
|
|
kfree(fw_data);
|
|
|
|
memcpy(tbuff + ret, cfg_buff, cfg_sz);
|
|
ret += cfg_sz;
|
|
|
|
fw_data = tbuff;
|
|
}
|
|
|
|
BT_INFO("cfg_sz %d, total size %d", cfg_sz, ret);
|
|
|
|
ret = rtl_download_firmware(hdev, fw_data, ret);
|
|
|
|
out:
|
|
release_firmware(fw);
|
|
kfree(fw_data);
|
|
err_req_fw:
|
|
if (cfg_sz)
|
|
kfree(cfg_buff);
|
|
return ret;
|
|
}
|
|
|
|
static struct sk_buff *btrtl_read_local_version(struct hci_dev *hdev)
|
|
{
|
|
struct sk_buff *skb;
|
|
|
|
skb = __hci_cmd_sync(hdev, HCI_OP_READ_LOCAL_VERSION, 0, NULL,
|
|
HCI_INIT_TIMEOUT);
|
|
if (IS_ERR(skb)) {
|
|
BT_ERR("%s: HCI_OP_READ_LOCAL_VERSION failed (%ld)",
|
|
hdev->name, PTR_ERR(skb));
|
|
return skb;
|
|
}
|
|
|
|
if (skb->len != sizeof(struct hci_rp_read_local_version)) {
|
|
BT_ERR("%s: HCI_OP_READ_LOCAL_VERSION event length mismatch",
|
|
hdev->name);
|
|
kfree_skb(skb);
|
|
return ERR_PTR(-EIO);
|
|
}
|
|
|
|
return skb;
|
|
}
|
|
|
|
int btrtl_setup_realtek(struct hci_dev *hdev)
|
|
{
|
|
struct sk_buff *skb;
|
|
struct hci_rp_read_local_version *resp;
|
|
u16 lmp_subver;
|
|
|
|
skb = btrtl_read_local_version(hdev);
|
|
if (IS_ERR(skb))
|
|
return -PTR_ERR(skb);
|
|
|
|
resp = (struct hci_rp_read_local_version *)skb->data;
|
|
BT_INFO("%s: rtl: examining hci_ver=%02x hci_rev=%04x lmp_ver=%02x "
|
|
"lmp_subver=%04x", hdev->name, resp->hci_ver, resp->hci_rev,
|
|
resp->lmp_ver, resp->lmp_subver);
|
|
|
|
lmp_subver = le16_to_cpu(resp->lmp_subver);
|
|
kfree_skb(skb);
|
|
|
|
/* Match a set of subver values that correspond to stock firmware,
|
|
* which is not compatible with standard btusb.
|
|
* If matched, upload an alternative firmware that does conform to
|
|
* standard btusb. Once that firmware is uploaded, the subver changes
|
|
* to a different value.
|
|
*/
|
|
switch (lmp_subver) {
|
|
case RTL_ROM_LMP_8723A:
|
|
case RTL_ROM_LMP_3499:
|
|
return btrtl_setup_rtl8723a(hdev);
|
|
case RTL_ROM_LMP_8723B:
|
|
return btrtl_setup_rtl8723b(hdev, lmp_subver,
|
|
"rtl_bt/rtl8723b_fw.bin");
|
|
case RTL_ROM_LMP_8821A:
|
|
return btrtl_setup_rtl8723b(hdev, lmp_subver,
|
|
"rtl_bt/rtl8821a_fw.bin");
|
|
case RTL_ROM_LMP_8761A:
|
|
return btrtl_setup_rtl8723b(hdev, lmp_subver,
|
|
"rtl_bt/rtl8761a_fw.bin");
|
|
case RTL_ROM_LMP_8822B:
|
|
return btrtl_setup_rtl8723b(hdev, lmp_subver,
|
|
"rtl_bt/rtl8822b_fw.bin");
|
|
default:
|
|
BT_INFO("rtl: assuming no firmware upload needed.");
|
|
return 0;
|
|
}
|
|
}
|
|
EXPORT_SYMBOL_GPL(btrtl_setup_realtek);
|
|
|
|
MODULE_AUTHOR("Daniel Drake <drake@endlessm.com>");
|
|
MODULE_DESCRIPTION("Bluetooth support for Realtek devices ver " VERSION);
|
|
MODULE_VERSION(VERSION);
|
|
MODULE_LICENSE("GPL");
|