text: // 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 "mtk-spi.h"
  
  #include <lib/device-protocol/pdev.h>
  #include <unistd.h>
  
  #include <ddk/binding.h>
  #include <ddk/debug.h>
  #include <ddk/metadata.h>
  #include <ddk/metadata/spi.h>
  #include <ddk/platform-defs.h>
  #include <fbl/alloc_checker.h>
  #include <fbl/auto_call.h>
  
  #include "registers.h"
  
  namespace spi {
  
  namespace {
  
  constexpr size_t kFifoAccessSize = 4;
  constexpr size_t kMaxFifoSize = 32;  // Depth of FIFO is 32 bytes.
  constexpr uint32_t kDummy = 0xFFFFFFFF;
  
  }  // namespace
  
  void MtkSpi::FifoTransferPacket(const uint8_t** tx, uint8_t** rx, size_t packet_size) {
    // Transfer
    {  // Fill FIFO
      for (size_t bytes_left = packet_size; bytes_left > 0;) {
        const size_t transfer_size = std::min(kFifoAccessSize, bytes_left);
  
        if (*tx) {
          uint32_t i32 = 0;
          memcpy(&i32, *tx, transfer_size);
          mmio_.Write32(i32, MTK_SPI_TX_DATA);
          *tx += transfer_size;
        } else {
          mmio_.Write32(kDummy, MTK_SPI_TX_DATA);
        }
  
        bytes_left -= transfer_size;
      }
  
      // Enable transfer
      CmdReg::Get().ReadFrom(&mmio_).set_activate(1).WriteTo(&mmio_);
    }
  
    // Wait for completion
    auto busy = Status1Reg::Get().ReadFrom(&mmio_);
    while (!busy.busy()) {
      busy.ReadFrom(&mmio_);
    };
  
    // Receive
    {
      for (size_t bytes_left = packet_size; bytes_left > 0;) {
        const size_t transfer_size = std::min(kFifoAccessSize, bytes_left);
  
        auto data = mmio_.Read32(MTK_SPI_RX_DATA);
        if (*rx) {
          memcpy(*rx, &data, transfer_size);
          *rx += transfer_size;
        }
  
        bytes_left -= transfer_size;
      }
    }
  }
  
  zx_status_t MtkSpi::FifoExchange(const uint8_t* txdata, uint8_t* out_rxdata, size_t data_size) {
    CmdReg::Get().ReadFrom(&mmio_).set_tx_dma_en(0).set_rx_dma_en(0).WriteTo(&mmio_);  // Disable DMA
  
    // Setup packet
    auto packet_size = std::min(kMaxFifoSize, data_size);
    auto packet_loop = static_cast<uint32_t>(data_size / packet_size);
    Cfg1Reg::Get()
        .ReadFrom(&mmio_)
        .set_packet_length(static_cast<uint32_t>(packet_size - 1))
        .set_packet_loop_count(packet_loop - 1)
        .WriteTo(&mmio_);
  
    const uint8_t* tx = txdata;
    uint8_t* rx = out_rxdata;
    for (uint32_t loop = 0; loop < packet_loop; loop++) {
      FifoTransferPacket(&tx, &rx, packet_size);
    }
  
    return (data_size % packet_size) ? FifoExchange(tx, rx, data_size % packet_size) : ZX_OK;
  }
  
  zx_status_t MtkSpi::SpiImplExchange(uint32_t cs, const uint8_t* txdata, size_t txdata_size,
                                      uint8_t* out_rxdata, size_t rxdata_size,
                                      size_t* out_rxdata_actual) {
    if (cs >= SpiImplGetChipSelectCount()) {
      return ZX_ERR_INVALID_ARGS;
    }
  
    if ((txdata_size && rxdata_size && (txdata_size != rxdata_size)) || (!txdata && !out_rxdata) ||
        (static_cast<bool>(txdata) ^ static_cast<bool>(txdata_size)) ||
        (static_cast<bool>(out_rxdata) ^ static_cast<bool>(rxdata_size))) {
      return ZX_ERR_INVALID_ARGS;
    }
    size_t data_size = txdata_size ? txdata_size : rxdata_size;
  
    memset(out_rxdata, 0, rxdata_size);
  
    zx_status_t status = ZX_OK;
    // Using FIFO for now, could also support DMA
    if ((status = FifoExchange(txdata, out_rxdata, data_size)) != ZX_OK) {
      zxlogf(ERROR, "%s: FifoExchange failed with %d", __func__, status);
      return status;
    }
  
    *out_rxdata_actual = rxdata_size;
  
    return ZX_OK;
  }
  
  zx_status_t MtkSpi::Init() {
    // Reset
    CmdReg::Get().ReadFrom(&mmio_).set_reset(1).WriteTo(&mmio_);
    CmdReg::Get().ReadFrom(&mmio_).set_reset(0).WriteTo(&mmio_);
  
    CmdReg::Get().ReadFrom(&mmio_).set_rx_msb_first(1).set_tx_msb_first(1).WriteTo(&mmio_);
  
    // Prepare transfer
    uint32_t div =
        (speed_hz_ < spi_clk_hz_ / 2) ? static_cast<uint32_t>((spi_clk_hz_ + 0.5) / speed_hz_) : 1;
    uint32_t sck_time = (div + 1) / 2;
    uint32_t cs_time = sck_time * 2;
    Cfg0Reg::Get()
        .ReadFrom(&mmio_)
        .set_cs_setup_count((cs_time - 1) & 0xFF)
        .set_cs_hold_count((cs_time - 1) & 0xFF)
        .WriteTo(&mmio_);
    Cfg2Reg::Get()
        .ReadFrom(&mmio_)
        .set_sck_low_count((sck_time - 1) & 0xFF)
        .set_sck_high_count((sck_time - 1) & 0xFF)
        .WriteTo(&mmio_);
    Cfg1Reg::Get().ReadFrom(&mmio_).set_cs_idle_count((cs_time - 1) & 0xFF).WriteTo(&mmio_);
  
    return ZX_OK;
  }
  
  zx_status_t MtkSpi::Create(void* ctx, zx_device_t* device) {
    ddk::PDev pdev(device);
    if (!pdev.is_valid()) {
      zxlogf(ERROR, "%s: Could not get pdev protocol", __func__);
      return ZX_ERR_NOT_SUPPORTED;
    }
  
    zx_status_t status = ZX_OK;
    size_t metadata_size, actual;
    if ((status = device_get_metadata_size(device, DEVICE_METADATA_SPI_CHANNELS, &metadata_size)) !=
        ZX_OK) {
      zxlogf(ERROR, "%s: device_get_metadata_size failed %d", __func__, status);
      return ZX_ERR_INTERNAL;
    }
    auto channel_count = metadata_size / sizeof(spi_channel_t);
    fbl::AllocChecker ac;
    std::unique_ptr<spi_channel_t[]> channels(new (&ac) spi_channel_t[channel_count]);
    if (!ac.check()) {
      zxlogf(ERROR, "%s: out of memory", __func__);
      return ZX_ERR_NO_MEMORY;
    }
    status = device_get_metadata(device, DEVICE_METADATA_SPI_CHANNELS, channels.get(), metadata_size,
                                 &actual);
    if (status != ZX_OK || actual != metadata_size) {
      zxlogf(ERROR, "%s: device_get_metadata failed %d", __func__, status);
      return ZX_ERR_INTERNAL;
    }
  
    for (uint32_t i = 0; i < channel_count; i++) {
      std::optional<ddk::MmioBuffer> mmio;
      if ((status = pdev.MapMmio(i, &mmio)) != ZX_OK) {
        zxlogf(ERROR, "%s: could not map mmio %d", __func__, status);
        return status;
      }
  
      fbl::AllocChecker ac;
      auto spi = fbl::make_unique_checked<MtkSpi>(&ac, device, std::move(mmio.value()));
      if (!ac.check()) {
        return ZX_ERR_NO_MEMORY;
      }
  
      if ((status = spi->Init()) != ZX_OK) {
        zxlogf(ERROR, "%s could not init %d", __func__, status);
        return status;
      }
  
      char devname[32];
      sprintf(devname, "mtk-spi-%d", channels[i].bus_id);
      status = spi->DdkAdd(devname);
      if (status != ZX_OK) {
        zxlogf(ERROR, "%s: DdkDeviceAdd failed for %s", __func__, devname);
        return status;
      }
      auto* ptr = spi.release();
  
      auto cleanup = fbl::MakeAutoCall([&ptr]() { ptr->DdkAsyncRemove(); });
  
      status = ptr->DdkAddMetadata(DEVICE_METADATA_PRIVATE, &channels[i].bus_id,
                                   sizeof channels[i].bus_id);
      if (status != ZX_OK) {
        zxlogf(ERROR, "%s: DdkAddMetadata failed for %s", __func__, devname);
        return status;
      }
  
      cleanup.cancel();
    }
  
    return ZX_OK;
  }
  
  static zx_driver_ops_t driver_ops = []() {
    zx_driver_ops_t ops = {};
    ops.version = DRIVER_OPS_VERSION;
    ops.bind = MtkSpi::Create;
    return ops;
  }();
  
  }  // namespace spi
  
  // clang-format off
  ZIRCON_DRIVER_BEGIN(mtk_spi, spi::driver_ops, "zircon", "0.1", 3)
      BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_PDEV),
      BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_MEDIATEK),
      BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_MEDIATEK_SPI),
  ZIRCON_DRIVER_END(mtk_spi)
  