| // Copyright 2019 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 "ot_radio_bootloader.h" |
| |
| #include <ctype.h> |
| #include <lib/async-loop/default.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/ddk/debug.h> |
| #include <lib/ddk/driver.h> |
| #include <lib/ddk/hw/arch_ops.h> |
| #include <lib/ddk/hw/reg.h> |
| #include <lib/ddk/metadata.h> |
| #include <lib/ddk/platform-defs.h> |
| #include <lib/driver-unit-test/utils.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <sys/types.h> |
| #include <zircon/compiler.h> |
| |
| #include <iostream> |
| |
| #include <fbl/algorithm.h> |
| #include <fbl/auto_lock.h> |
| #include <fbl/ref_counted.h> |
| #include <fbl/ref_ptr.h> |
| |
| namespace ot { |
| |
| // mimic device CRC32 computation in bootloader mode |
| constexpr uint32_t kCrcPolynomial = 0xEDB88320L; |
| uint32_t OtRadioDeviceBootloader::BlModeCrc32(uint32_t crc, const void *b, size_t len) { |
| const uint8_t *buf = reinterpret_cast<const uint8_t *>(b); |
| crc = ~crc; |
| while (len--) { |
| int j; |
| uint8_t c = *buf++; |
| crc = crc ^ c; |
| for (j = 0; j < 8; j++) { |
| crc = (crc >> 1) ^ (-(int)(crc & 1) & kCrcPolynomial); |
| } |
| } |
| return ~crc; |
| } |
| |
| zx_status_t OtRadioDeviceBootloader::GetLastZxStatus() { return bl_zx_status; } |
| |
| void OtRadioDeviceBootloader::BlPrepareCmd(NlSpiBlBasicCmd *cmdp, uint16_t length, uint16_t cmd) { |
| cmdp->length = length; |
| cmdp->cmd = cmd; |
| cmdp->crc32 = BlModeCrc32(kSpiPacketCrc32InitValue, &cmdp->length, length - sizeof(cmdp->crc32)); |
| } |
| |
| void OtRadioDeviceBootloader::PrintSpiCommand(int cmd, void *cmd_ptr, size_t cmd_size) { |
| zxlogf(DEBUG, "ot-radio: spi command "); |
| |
| switch (cmd) { |
| case BL_SPI_CMD_INVALID: |
| zxlogf(DEBUG, "BL_SPI_CMD_INVALID: "); |
| break; |
| case BL_SPI_CMD_GET_VERSION: |
| zxlogf(DEBUG, "BL_SPI_CMD_GET_VERSION: "); |
| break; |
| case BL_SPI_CMD_UNUSED: |
| zxlogf(DEBUG, "BL_SPI_CMD_UNUSED: "); |
| break; |
| case BL_SPI_CMD_FLASH_ERASE: |
| zxlogf(DEBUG, "BL_SPI_CMD_FLASH_ERASE: "); |
| break; |
| case BL_SPI_CMD_FLASH_WRITE: |
| zxlogf(DEBUG, "BL_SPI_CMD_FLASH_WRITE: "); |
| break; |
| case BL_SPI_CMD_FLASH_VERIFY: |
| zxlogf(DEBUG, "BL_SPI_CMD_FLASH_VERIFY: "); |
| break; |
| default: |
| zxlogf(DEBUG, "UNKNOWN CMD: "); |
| } |
| |
| unsigned long i; |
| uint8_t *cmd_buf = reinterpret_cast<uint8_t *>(cmd_ptr); |
| for (i = 0; i < cmd_size; i++) { |
| zxlogf(DEBUG, "0x%x ", cmd_buf[i]); |
| } |
| zxlogf(DEBUG, ""); |
| } |
| |
| OtRadioBlResult OtRadioDeviceBootloader::SendSpiCmdAndGetResponse(uint8_t *cmd, size_t cmd_size, |
| size_t exp_resp_size) { |
| zxlogf(DEBUG, "ot-radio: Sending command:"); |
| PrintSpiCommand(reinterpret_cast<NlSpiBlBasicCmd *>(cmd)->cmd, cmd, cmd_size); |
| |
| zx_status_t status = dev_handle_->spi_.Transmit(cmd, cmd_size); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: spi transmit failed with status : %d for cmd : %d", status, |
| reinterpret_cast<NlSpiBlBasicCmd *>(cmd)->cmd); |
| bl_zx_status = status; |
| return BL_ERR_SPI_TRANSMIT_FAILED; |
| } |
| |
| const uint32_t wait_time_sec = 10; |
| |
| while (1) { |
| zx_port_packet_t packet = {}; |
| auto status = dev_handle_->port_.wait(zx::deadline_after(zx::sec(wait_time_sec)), &packet); |
| |
| if (status == ZX_ERR_TIMED_OUT) { |
| zxlogf(ERROR, "ot-radio: port wait timed out: %d", status); |
| bl_zx_status = ZX_ERR_TIMED_OUT; |
| return BL_ERR_PORT_WAIT_TIMED_OUT; |
| } else if (status != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: port wait failed: %d", status); |
| bl_zx_status = status; |
| return BL_ERR_PORT_WAIT_FAILED; |
| } |
| |
| if (packet.key == PORT_KEY_EXIT_THREAD) { |
| zxlogf(ERROR, "ot-radio: port key exit thread received"); |
| return BL_ERR_PORT_KEY_EXIT_THREAD; |
| } else if (packet.key == PORT_KEY_RADIO_IRQ) { |
| dev_handle_->interrupt_.ack(); |
| zxlogf(DEBUG, "ot-radio: interrupt"); |
| |
| // Read packet |
| uint8_t i; |
| size_t rx_actual; |
| size_t read_length = exp_resp_size; |
| dev_handle_->spi_.Receive(read_length, &dev_handle_->spi_rx_buffer_[0], read_length, |
| &rx_actual); |
| zxlogf(DEBUG, "ot-radio: rx_actual %lu expected : %lu", rx_actual, read_length); |
| for (i = 0; i < read_length; i++) { |
| zxlogf(DEBUG, "ot-radio: RX %2X %c", dev_handle_->spi_rx_buffer_[i], |
| isalnum(dev_handle_->spi_rx_buffer_[i]) ? dev_handle_->spi_rx_buffer_[i] : '#'); |
| } |
| |
| // Extract cmd field out of cmd that was sent |
| uint8_t cmd_sent = reinterpret_cast<NlSpiBlBasicCmd *>(cmd)->cmd; |
| |
| // Check if response corresponds to appropriate command. |
| // Reinterpret as cmd as first field in basic_response is basic_cmd, which |
| // contains all the necessary fields |
| NlSpiBlBasicCmd *response = |
| reinterpret_cast<NlSpiBlBasicCmd *>(&dev_handle_->spi_rx_buffer_[0]); |
| |
| // Special case to ignore -- in some cases it is found a spurious response |
| // of all 0xff's is received. Ignore such responses for now |
| if (response->cmd == 0xff) { |
| zxlogf(DEBUG, "ot-radio: response received has cmd = 0xff, continuing"); |
| continue; |
| } |
| |
| cmd_sent = ~cmd_sent; |
| if (response->cmd != cmd_sent) { |
| zxlogf(ERROR, "ot-radio: response received has cmd(%u) != cmd_sent(%u)", response->cmd, |
| cmd_sent); |
| return BL_ERR_RESP_CMD_MISMATCH; |
| } |
| |
| if (response->length != exp_resp_size) { |
| zxlogf(ERROR, "ot-radio: response received has length(%hu) != expected length(%zu)", |
| response->length, exp_resp_size); |
| return BL_ERR_RESP_LENGTH_MISMATCH; |
| } |
| |
| uint32_t expected_crc = BlModeCrc32(kSpiPacketCrc32InitValue, &response->length, |
| exp_resp_size - sizeof(response->crc32)); |
| if (response->crc32 != expected_crc) { |
| zxlogf(ERROR, "ot-radio: response received has crc(%u) != expected crc(%u)", |
| response->crc32, expected_crc); |
| return BL_ERR_RESP_CRC_MISMATCH; |
| } |
| |
| break; |
| } |
| } |
| return BL_RET_SUCCESS; |
| } |
| |
| bool OtRadioDeviceBootloader::FirmwareAlreadyUpToDateCRC(const std::vector<uint8_t> &fw_bytes) { |
| #ifdef INTERNAL_ACCESS |
| return (VerifyUpload(fw_bytes) == 0); |
| #else |
| return true; |
| #endif |
| } |
| |
| zx_status_t OtRadioDeviceBootloader::PutRcpInBootloader() { |
| zx_status_t status = ZX_OK; |
| |
| zxlogf(DEBUG, "ot-radio : putting rcp in bootloader"); |
| |
| status = dev_handle_->gpio_[OT_RADIO_BOOTLOADER_PIN].Write(0); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: gpio write failed"); |
| return status; |
| } |
| zx::nanosleep(zx::deadline_after(zx::msec(50))); |
| |
| status = dev_handle_->gpio_[OT_RADIO_RESET_PIN].Write(0); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: gpio write failed"); |
| return status; |
| } |
| zx::nanosleep(zx::deadline_after(zx::msec(50))); |
| |
| status = dev_handle_->gpio_[OT_RADIO_RESET_PIN].Write(1); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: gpio write failed"); |
| return status; |
| } |
| |
| // Note - give some time before releasing bootloader pin otherwise RCP |
| // doesn't go into bootloader mode. This is because after reset, code |
| // starts executing on RCP which checks for BOOTLOADER pin status, if |
| // the BOOTLOADER pin is deasserted by that time, the code will proceed |
| // assuming normal operation. |
| // This also gives time for the bootloader to be up and ready to respond |
| zx::nanosleep(zx::deadline_after(zx::msec(400))); |
| |
| status = dev_handle_->gpio_[OT_RADIO_BOOTLOADER_PIN].Write(1); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: gpio write failed"); |
| return status; |
| } |
| |
| zxlogf(DEBUG, "ot-radio : device has been put in bootloader mode"); |
| |
| return status; |
| } |
| |
| OtRadioBlResult OtRadioDeviceBootloader::GetBootloaderVersion(std::string &bl_version) { |
| OtRadioBlResult result = BL_RET_SUCCESS; |
| |
| // Now will attempt to get bootloader version |
| NlSpiBlBasicCmd get_version; |
| BlPrepareCmd(&get_version, sizeof(get_version), BL_SPI_CMD_GET_VERSION); |
| |
| // Send get_bootloader_version command |
| result = SendSpiCmdAndGetResponse(reinterpret_cast<uint8_t *>(&get_version), sizeof(get_version), |
| sizeof(NlSpiBlGetVersionResponse)); |
| if (result != BL_RET_SUCCESS) { |
| zxlogf(ERROR, |
| "ot-radio: Error in sending get-bootloader version command and getting response\n"); |
| return result; |
| } |
| |
| NlSpiBlGetVersionResponse *response = |
| reinterpret_cast<NlSpiBlGetVersionResponse *>(&dev_handle_->spi_rx_buffer_[0]); |
| |
| // Ensure that final byte in version is null (just in case response was buggy) |
| response->version[kNlBootloaderVersionMaxLength - 1] = '\0'; |
| |
| // Copy the version received |
| bl_version.assign(reinterpret_cast<char *>(response->version)); |
| |
| return result; |
| } |
| |
| zx_status_t OtRadioDeviceBootloader::GetFirmwareBytes(std::vector<uint8_t> *fw_bytes) { |
| zx_handle_t vmo = ZX_HANDLE_INVALID; |
| size_t size; |
| |
| zx_status_t load_fw_status = |
| load_firmware(dev_handle_->parent(), OT_NCP_FIRMWARE_BIN, &vmo, &size); |
| |
| if (load_fw_status == ZX_OK) { |
| zxlogf(DEBUG, "ot-radio: load_firmware succeeded"); |
| fw_bytes->resize(size); |
| zx_status_t vmo_read_status = zx_vmo_read(vmo, &(fw_bytes->front()), 0, size); |
| |
| if (vmo_read_status != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: failed to read vmo : %d", vmo_read_status); |
| return vmo_read_status; |
| } |
| } else { |
| zxlogf(ERROR, "ot-radio: load_firmware failed with error : %d", load_fw_status); |
| return load_fw_status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| OtRadioBlResult OtRadioDeviceBootloader::SendFlashEraseCmd(uint32_t address, uint32_t length) { |
| NlSpiBlCmdFlashErase erase_cmd; |
| erase_cmd.address = address; |
| erase_cmd.length = length; |
| BlPrepareCmd(&(erase_cmd.cmd), sizeof(erase_cmd), BL_SPI_CMD_FLASH_ERASE); |
| |
| OtRadioBlResult result; |
| result = SendSpiCmdAndGetResponse(reinterpret_cast<uint8_t *>(&erase_cmd), sizeof(erase_cmd), |
| sizeof(NlSpiBlBasicResponse)); |
| if (result != BL_RET_SUCCESS) { |
| zxlogf(ERROR, "ot-radio: Error in sending flash-erase command and getting response"); |
| } |
| |
| return result; |
| } |
| |
| OtRadioBlResult OtRadioDeviceBootloader::UploadFirmware(const std::vector<uint8_t> &fw_bytes) { |
| unsigned long bytes_left = fw_bytes.size(); |
| unsigned retry_count = 0; |
| const unsigned max_retry_count = 10; |
| const uint8_t *current_ptr = reinterpret_cast<const uint8_t *>(&fw_bytes[0]); |
| uint32_t address = kFwStartAddr; |
| while ((bytes_left > 0) && (retry_count < max_retry_count)) { |
| NlSpiBlCmdFlashWrite write_cmd; |
| size_t bytes_to_write = sizeof(write_cmd.data); |
| if (bytes_to_write > bytes_left) { |
| bytes_to_write = bytes_left; |
| } |
| |
| // Update various fields of write_cmd like address and data |
| write_cmd.address = address; |
| memcpy(write_cmd.data, current_ptr, bytes_to_write); |
| |
| // The actual size of spi command (can be less than sizeof(write_cmd)) |
| size_t spi_cmd_size = sizeof(write_cmd.cmd) + sizeof(write_cmd.address) + bytes_to_write; |
| |
| // Now prepare the cmd since rest of the fields are updated. As crc can be |
| // calculated now |
| BlPrepareCmd(&write_cmd.cmd, spi_cmd_size, BL_SPI_CMD_FLASH_WRITE); |
| zxlogf(DEBUG, "ot-radio: writing flash @ 0x%08x", address); |
| |
| OtRadioBlResult result; |
| result = SendSpiCmdAndGetResponse(reinterpret_cast<uint8_t *>(&write_cmd), spi_cmd_size, |
| sizeof(NlSpiBlBasicResponse)); |
| if (result != BL_RET_SUCCESS) { |
| zxlogf(ERROR, "ot-radio: Sending flash write command got invalid response"); |
| return result; |
| } |
| |
| // Response is stored in dev_handle_->spi_rx_buffer_ so read that |
| NlSpiBlBasicResponse *basic_response = |
| reinterpret_cast<NlSpiBlBasicResponse *>(&dev_handle_->spi_rx_buffer_[0]); |
| |
| if (basic_response->status != BL_ERROR_NONE) { |
| zxlogf(ERROR, "ot-radio: Flash write error @ 0x%x, status=%d", address, |
| basic_response->status); |
| retry_count++; |
| if (retry_count == max_retry_count) { |
| return BL_ERR_WR_CMD_FAILED; |
| } else { |
| zxlogf(ERROR, "ot-radio: will retry %d more times", max_retry_count - retry_count); |
| continue; |
| } |
| } else { |
| retry_count = 0; |
| // advance state |
| address += bytes_to_write; |
| current_ptr += bytes_to_write; |
| bytes_left -= bytes_to_write; |
| } |
| } |
| |
| return BL_RET_SUCCESS; |
| } |
| |
| // Verify the uploaded firmware by sending verify command to bootloader |
| // First compute the crc for entire firmware bytes, and then send the crc to |
| // bootloader to verify on it's end |
| OtRadioBlResult OtRadioDeviceBootloader::VerifyUpload(const std::vector<uint8_t> &fw_bytes) { |
| NlSpiCmdFlashVerify verify_cmd; |
| verify_cmd.address = kFwStartAddr; |
| verify_cmd.length = fw_bytes.size(); |
| verify_cmd.crc32_check = BlModeCrc32(kSpiPacketCrc32InitValue, &fw_bytes[0], fw_bytes.size()); |
| BlPrepareCmd(&verify_cmd.cmd, sizeof(verify_cmd), BL_SPI_CMD_FLASH_VERIFY); |
| zxlogf(DEBUG, "ot-radio: Verifying crc32 of flashed image"); |
| |
| OtRadioBlResult result; |
| result = SendSpiCmdAndGetResponse(reinterpret_cast<uint8_t *>(&verify_cmd), sizeof(verify_cmd), |
| sizeof(NlSpiBlBasicResponse)); |
| if (result != BL_RET_SUCCESS) { |
| zxlogf(ERROR, "ot-radio: error in sending flash verify cmd and getting a response"); |
| return result; |
| } |
| |
| NlSpiBlBasicResponse *basic_response = |
| reinterpret_cast<NlSpiBlBasicResponse *>(&dev_handle_->spi_rx_buffer_[0]); |
| |
| if (basic_response->status != BL_ERROR_NONE) { |
| zxlogf(ERROR, "ot-radio: Verification failed"); |
| return BL_ERR_VERIFICATION_FAILED; |
| } |
| |
| return BL_RET_SUCCESS; |
| } |
| |
| OtRadioBlResult OtRadioDeviceBootloader::UploadAndCheckFirmware( |
| const std::vector<uint8_t> &fw_bytes) { |
| OtRadioBlResult status; |
| |
| status = SendFlashEraseCmd(kFwStartAddr, fw_bytes.size()); |
| if (status != BL_RET_SUCCESS) { |
| zxlogf(ERROR, "ot-radio: Flash Erase Command failed"); |
| return status; |
| } |
| |
| status = UploadFirmware(fw_bytes); |
| if (status != BL_RET_SUCCESS) { |
| zxlogf(ERROR, "ot-radio: Upload Firmware command failed"); |
| return status; |
| } |
| |
| status = VerifyUpload(fw_bytes); |
| if (status != BL_RET_SUCCESS) { |
| zxlogf(ERROR, "ot-radio: Verify Upload failed"); |
| return status; |
| } |
| |
| return BL_RET_SUCCESS; |
| } |
| |
| OtRadioBlResult OtRadioDeviceBootloader::UpdateRadioFirmware() { |
| #ifndef INTERNAL_ACCESS |
| assert(0 && "Should not reach here without internal access"); |
| #endif |
| |
| bl_zx_status = ZX_OK; |
| |
| if (GetNewFirmwareVersion().size() == 0) { |
| // Invalid version string indicates invalid firmware |
| zxlogf(ERROR, "ot-radio: The new firmware is invalid"); |
| return BL_ERR_INVALID_FW_VERSION; |
| } |
| |
| std::vector<uint8_t> fw_bytes; |
| zx_status_t status; |
| if ((status = GetFirmwareBytes(&fw_bytes)) != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: GetFirmwareBytes failed with status : %d", status); |
| bl_zx_status = status; |
| return BL_ERR_GET_FW_BYTES_FAILED; |
| } |
| |
| status = PutRcpInBootloader(); |
| if (status != ZX_OK) { |
| bl_zx_status = status; |
| // Attempt a Reset to try and clear up GPIO state: |
| if ((status = dev_handle_->Reset()) != ZX_OK) { |
| zxlogf(ERROR, "ot-radio: subsequent Reset call also failed, status: %d", status); |
| } |
| return BL_ERR_PUT_RCP_BLMODE_FAIL; |
| } |
| |
| OtRadioBlResult result; |
| result = UploadAndCheckFirmware(fw_bytes); |
| // If result indicates an error, we should still reset the device first |
| // before returning the error. So don't return early here. |
| |
| status = dev_handle_->Reset(); |
| if (status != ZX_OK) { |
| bl_zx_status = status; |
| return BL_ERR_RESET_FAILED; |
| } |
| |
| return result; |
| } |
| |
| } // namespace ot |