| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include <assert.h> |
| #include <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/device.h> |
| #include <ddk/driver.h> |
| #include <ddk/protocol/bt-hci.h> |
| #include <ddk/protocol/platform-defs.h> |
| #include <ddk/protocol/serial.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <threads.h> |
| #include <unistd.h> |
| #include <zircon/device/bt-hci.h> |
| #include <zircon/status.h> |
| #include <zircon/threads.h> |
| |
| // HCI reset |
| const uint8_t RESET_CMD[] = { 0x3, 0xc, 0x0 }; |
| |
| // vendor command to switch baud rate to 2000000 |
| const uint8_t SET_BAUD_RATE_CMD[] = { 0x18, 0xfc, 0x6, 0x0, 0x0, 0x80, 0x84, 0x1e, 0x0 }; |
| |
| const uint8_t START_FIRMWARE_DOWNLOAD_CMD[] = { 0x2e, 0xfc, 0x0 }; |
| |
| #define HCI_EVT_COMMAND_COMPLETE 0x0e |
| |
| typedef struct { |
| zx_device_t* zxdev; |
| zx_device_t* transport_dev; |
| bt_hci_protocol_t hci; |
| serial_protocol_t serial; |
| zx_handle_t command_channel; |
| } bcm_uart_hci_t; |
| |
| static zx_status_t bcm_hci_get_protocol(void* ctx, uint32_t proto_id, void* out_proto) { |
| if (proto_id != ZX_PROTOCOL_BT_HCI) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| bcm_uart_hci_t* hci = ctx; |
| bt_hci_protocol_t* hci_proto = out_proto; |
| |
| // Forward the underlying bt-transport ops. |
| hci_proto->ops = hci->hci.ops; |
| hci_proto->ctx = hci->hci.ctx; |
| |
| return ZX_OK; |
| } |
| |
| static zx_status_t bcm_hci_ioctl(void* ctx, uint32_t op, const void* in_buf, size_t in_len, |
| void* out_buf, size_t out_len, size_t* out_actual) { |
| bcm_uart_hci_t* hci = ctx; |
| if (out_len < sizeof(zx_handle_t)) { |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| |
| zx_handle_t* reply = out_buf; |
| |
| zx_status_t status = ZX_ERR_NOT_SUPPORTED; |
| if (op == IOCTL_BT_HCI_GET_COMMAND_CHANNEL) { |
| if (hci->command_channel != ZX_HANDLE_INVALID) { |
| *reply = hci->command_channel; |
| hci->command_channel = ZX_HANDLE_INVALID; |
| status = ZX_OK; |
| } else { |
| status = bt_hci_open_command_channel(&hci->hci, reply); |
| } |
| } else if (op == IOCTL_BT_HCI_GET_ACL_DATA_CHANNEL) { |
| status = bt_hci_open_acl_data_channel(&hci->hci, reply); |
| } else if (op == IOCTL_BT_HCI_GET_SNOOP_CHANNEL) { |
| status = bt_hci_open_snoop_channel(&hci->hci, reply); |
| } |
| |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| *out_actual = sizeof(*reply); |
| return ZX_OK; |
| } |
| |
| static void bcm_hci_unbind(void* ctx) { |
| bcm_uart_hci_t* hci = ctx; |
| |
| device_remove(hci->zxdev); |
| } |
| |
| static void bcm_hci_release(void* ctx) { |
| free(ctx); |
| } |
| |
| static zx_protocol_device_t bcm_hci_device_proto = { |
| .version = DEVICE_OPS_VERSION, |
| .get_protocol = bcm_hci_get_protocol, |
| .ioctl = bcm_hci_ioctl, |
| .unbind = bcm_hci_unbind, |
| .release = bcm_hci_release, |
| }; |
| |
| |
| static zx_status_t bcm_hci_send_command(bcm_uart_hci_t* hci, const uint8_t* command, size_t length) { |
| // send HCI command |
| zx_status_t status = zx_channel_write(hci->command_channel, 0, command, length, NULL, 0); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "bcm_hci_send_command zx_channel_write failed %d\n", status); |
| return status; |
| } |
| |
| // wait for command complete |
| uint8_t event_buf[255 + 2]; |
| uint32_t actual; |
| |
| do { |
| status = zx_channel_read(hci->command_channel, 0, event_buf, NULL, sizeof(event_buf), 0, |
| &actual, NULL); |
| if (status == ZX_ERR_SHOULD_WAIT) { |
| zx_object_wait_one(hci->command_channel, ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, |
| ZX_TIME_INFINITE, NULL); |
| } |
| } while (status == ZX_ERR_SHOULD_WAIT); |
| |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "bcm_hci_send_command zx_channel_read failed %d\n", status); |
| return status; |
| } |
| |
| if (event_buf[0] != HCI_EVT_COMMAND_COMPLETE || event_buf[1] != 4 || event_buf[3] != command[0] |
| || event_buf[4] != command[1]) { |
| zxlogf(ERROR, "bcm_hci_send_command did not receive command complete\n"); |
| return ZX_ERR_INTERNAL; |
| } |
| if (event_buf[5] != 0) { |
| zxlogf(ERROR, "bcm_hci_send_command got command complete error %u\n", event_buf[5]); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| return ZX_OK; |
| } |
| |
| static int bcm_hci_start_thread(void* arg) { |
| bcm_uart_hci_t* hci = arg; |
| zx_handle_t fw_vmo; |
| |
| zx_status_t status = bt_hci_open_command_channel(&hci->hci, &hci->command_channel); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| // Send Reset command |
| status = bcm_hci_send_command(hci, RESET_CMD, sizeof(RESET_CMD)); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| // switch baud rate to 2000000 |
| status = bcm_hci_send_command(hci, SET_BAUD_RATE_CMD, sizeof(SET_BAUD_RATE_CMD)); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| status = serial_config(&hci->serial, 0, 2000000, SERIAL_SET_BAUD_RATE_ONLY); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| size_t fw_size; |
| status = load_firmware(hci->zxdev, "/boot/firmware/bt-firmware.bin", &fw_vmo, &fw_size); |
| if (status == ZX_OK) { |
| status = bcm_hci_send_command(hci, START_FIRMWARE_DOWNLOAD_CMD, sizeof(START_FIRMWARE_DOWNLOAD_CMD)); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(50))); |
| |
| zx_off_t offset = 0; |
| while (offset < fw_size) { |
| uint8_t buffer[255 + 3]; |
| size_t actual; |
| |
| status = zx_vmo_read(fw_vmo, buffer, offset, sizeof(buffer), &actual); |
| if (status != ZX_OK) { |
| goto vmo_close_fail; |
| } |
| if (actual < 3) { |
| zxlogf(ERROR, "short HCI command in firmware download\n"); |
| status = ZX_ERR_INTERNAL; |
| goto vmo_close_fail; |
| } |
| size_t length = buffer[2] + 3; |
| if (actual < length) { |
| zxlogf(ERROR, "short HCI command in firmware download\n"); |
| status = ZX_ERR_INTERNAL; |
| goto vmo_close_fail; |
| } |
| status = bcm_hci_send_command(hci, buffer, length); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "bcm_hci_send_command failed in firmware download: %d\n", status); |
| goto vmo_close_fail; |
| } |
| offset += length; |
| } |
| |
| zx_handle_close(fw_vmo); |
| |
| // firmware switched us back to 115200. switch back to 2000000 |
| status = serial_config(&hci->serial, 0, 115200, SERIAL_SET_BAUD_RATE_ONLY); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| // switch baud rate to 2000000 again |
| status = bcm_hci_send_command(hci, SET_BAUD_RATE_CMD, sizeof(SET_BAUD_RATE_CMD)); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| |
| status = serial_config(&hci->serial, 0, 2000000, SERIAL_SET_BAUD_RATE_ONLY); |
| if (status != ZX_OK) { |
| goto fail; |
| } |
| } else { |
| zxlogf(INFO, "bcm-uart-hci: no firmware file found\n"); |
| } |
| |
| device_make_visible(hci->zxdev); |
| return 0; |
| |
| vmo_close_fail: |
| zx_handle_close(fw_vmo); |
| fail: |
| zxlogf(ERROR, "bcm_hci_start_thread: device initialization failed: %d\n", status); |
| device_remove(hci->zxdev); |
| return -1; |
| } |
| |
| static zx_status_t bcm_hci_bind(void* ctx, zx_device_t* device) { |
| bcm_uart_hci_t* hci = calloc(1, sizeof(bcm_uart_hci_t)); |
| if (!hci) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| zx_status_t status = device_get_protocol(device, ZX_PROTOCOL_BT_HCI, &hci->hci); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "bcm_hci_bind: get protocol ZX_PROTOCOL_BT_HCI failed\n"); |
| return status; |
| } |
| status = device_get_protocol(device, ZX_PROTOCOL_SERIAL, &hci->serial); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "bcm_hci_bind: get protocol ZX_PROTOCOL_SERIAL failed\n"); |
| return status; |
| } |
| |
| device_add_args_t args = { |
| .version = DEVICE_ADD_ARGS_VERSION, |
| .name = "bcm-uart-hci", |
| .ctx = hci, |
| .ops = &bcm_hci_device_proto, |
| .proto_id = ZX_PROTOCOL_BT_HCI, |
| .flags = DEVICE_ADD_INVISIBLE, |
| }; |
| |
| hci->transport_dev = device; |
| |
| status = device_add(device, &args, &hci->zxdev); |
| if (status != ZX_OK) { |
| bcm_hci_release(hci); |
| return status; |
| } |
| |
| // create thread to continue initialization |
| thrd_t t; |
| int thrd_rc = thrd_create_with_name(&t, bcm_hci_start_thread, hci, "bcm_hci_start_thread"); |
| if (thrd_rc != thrd_success) { |
| device_remove(hci->zxdev); |
| bcm_hci_release(hci); |
| return thrd_status_to_zx_status(thrd_rc); |
| } |
| |
| return ZX_OK; |
| } |
| |
| static zx_driver_ops_t bcm_hci_driver_ops = { |
| .version = DRIVER_OPS_VERSION, |
| .bind = bcm_hci_bind, |
| }; |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(bcm_hci, bcm_hci_driver_ops, "fuchsia", "0.1", 3) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_BT_TRANSPORT), |
| BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_BROADCOM), |
| BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_PID, PDEV_PID_BCM4356), |
| ZIRCON_DRIVER_END(bcm_hci) |