| // 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 "mt8167-i2c.h" |
| |
| #include <fuchsia/hardware/i2cimpl/c/banjo.h> |
| #include <fuchsia/hardware/platform/device/c/banjo.h> |
| #include <lib/device-protocol/pdev.h> |
| #include <lib/device-protocol/platform-device.h> |
| #include <zircon/syscalls/port.h> |
| #include <zircon/types.h> |
| |
| #include <ddk/debug.h> |
| #include <ddk/device.h> |
| #include <ddk/platform-defs.h> |
| #include <fbl/alloc_checker.h> |
| #include <fbl/auto_call.h> |
| |
| #include "src/devices/i2c/drivers/mt8167-i2c/mt8167_i2c_bind.h" |
| |
| //#define TEST_USB_REGS_READ |
| |
| namespace mt8167_i2c { |
| |
| constexpr size_t kMaxTransferSize = UINT16_MAX - 1; // More than enough. |
| constexpr size_t kHwFifoSize = 8; |
| constexpr uint32_t kEventCompletion = ZX_USER_SIGNAL_0; |
| constexpr zx::duration kTimeout = zx::msec(10); |
| constexpr uint32_t kAltFunctionGpio = 0; |
| constexpr uint32_t kAltFunctionI2c = 1; |
| |
| uint32_t Mt8167I2c::I2cImplGetBusCount() { return bus_count_; } |
| |
| zx_status_t Mt8167I2c::I2cImplGetMaxTransferSize(uint32_t bus_id, size_t* out_size) { |
| *out_size = kMaxTransferSize; |
| return ZX_OK; |
| } |
| |
| zx_status_t Mt8167I2c::I2cImplSetBitrate(uint32_t bus_id, uint32_t bitrate) { |
| // TODO(andresoportus): Support changing frequencies. |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t Mt8167I2c::I2cImplTransact(uint32_t id, const i2c_impl_op_t* ops, size_t count) { |
| zx_status_t status = ZX_OK; |
| if (id >= bus_count_) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| auto control_reg = ControlReg::Get().ReadFrom(&keys_[id].mmio); |
| control_reg.set_ackerr_det_en(1).set_clk_ext_en(1).WriteTo(&keys_[id].mmio); |
| |
| for (size_t i = 0; i < count; ++i) { |
| if (ops[i].address > 0xFF) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| uint8_t addr = static_cast<uint8_t>(ops[i].address); |
| // TODO(andresoportus): Add support for HW transaction (write followed by read). |
| status = Transact(ops[i].is_read, id, addr, ops[i].data_buffer, ops[i].data_size, ops[i].stop); |
| if (status != ZX_OK && bind_finished_) { |
| zxlogf(ERROR, "%s: error in bus id: %u addr: 0x%X size: %lu", __func__, id, addr, |
| ops[i].data_size); |
| Reset(id); |
| return status; |
| } |
| } |
| |
| return ZX_OK; |
| } |
| |
| int Mt8167I2c::IrqThread() { |
| zx_port_packet_t packet; |
| while (1) { |
| auto status = irq_port_.wait(zx::time::infinite(), &packet); |
| zxlogf(DEBUG, "Port key %lu triggered", packet.key); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: irq_port_.wait failed %d ", __func__, status); |
| return status; |
| } |
| auto id = static_cast<uint32_t>(packet.key); |
| ZX_ASSERT(id < keys_.size()); |
| keys_[id].irq.ack(); |
| keys_[id].event.signal(0, kEventCompletion); |
| } |
| } |
| |
| void Mt8167I2c::Reset(uint32_t id) { |
| SoftResetReg::Get().ReadFrom(&keys_[id].mmio).set_soft_reset(1).WriteTo(&keys_[id].mmio); |
| IntrStatReg::Get().FromValue(0xFFFFFFFF).WriteTo(&keys_[id].mmio); // Write to clear register. |
| } |
| |
| void Mt8167I2c::DataMove(bool is_read, uint32_t id, void* buf, size_t len) { |
| uint8_t* p = static_cast<uint8_t*>(buf); |
| for (size_t i = 0; i < len; ++i) { |
| if (is_read) { |
| p[i] = DataPortReg::Get().ReadFrom(&keys_[id].mmio).reg_value(); |
| } else { |
| DataPortReg::Get().FromValue(p[i]).WriteTo(&keys_[id].mmio); |
| } |
| } |
| } |
| |
| zx_status_t Mt8167I2c::Transact(bool is_read, uint32_t id, uint8_t addr, void* buf, size_t len, |
| bool stop) { |
| zx_status_t status; |
| |
| // TODO(andresoportus): Only stop when stop is set. |
| // TODO(andresoportus): Add support for arbitrary sizes. |
| if (len > kHwFifoSize) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| uint8_t addr_dir = static_cast<uint8_t>((addr << 1) | is_read); |
| FifoAddrClrReg::Get().ReadFrom(&keys_[id].mmio).set_fifo_addr_clr(1).WriteTo(&keys_[id].mmio); |
| SlaveAddrReg::Get().ReadFrom(&keys_[id].mmio).set_reg_value(addr_dir).WriteTo(&keys_[id].mmio); |
| TransferLenReg::Get().FromValue(static_cast<uint8_t>(len)).WriteTo(&keys_[id].mmio); |
| TransacLenReg::Get().FromValue(1).WriteTo(&keys_[id].mmio); // Single transaction of len bytes. |
| |
| IntrStatReg::Get().FromValue(0xFFFFFFFF).WriteTo(&keys_[id].mmio); // Write to clear register. |
| |
| if (!is_read) { |
| DataMove(is_read, id, buf, len); |
| } |
| |
| StartReg::Get().ReadFrom(&keys_[id].mmio).set_start(1).WriteTo(&keys_[id].mmio); |
| status = keys_[id].event.wait_one(kEventCompletion, zx::deadline_after(kTimeout), nullptr); |
| if (status != ZX_OK) { |
| return status; |
| } |
| status = keys_[id].event.signal(kEventCompletion, 0); |
| if (status != ZX_OK) { |
| return status; |
| } |
| if (is_read) { |
| DataMove(is_read, id, buf, len); |
| } |
| auto st = IntrStatReg::Get().ReadFrom(&keys_[id].mmio); |
| if (st.arb_lost() || st.hs_nacker() || st.ackerr()) { |
| if (bind_finished_) { |
| zxlogf(ERROR, "%s: I2C error 0x%X", __func__, |
| IntrStatReg::Get().ReadFrom(&keys_[id].mmio).reg_value()); |
| if (st.ackerr()) { |
| zxlogf(ERROR, "%s: No I2C ack reply from peripheral", __func__); |
| } |
| } |
| return ZX_ERR_INTERNAL; |
| } |
| return ZX_OK; |
| } |
| |
| void Mt8167I2c::ShutDown() { |
| for (uint32_t id = 0; id < bus_count_; id++) { |
| keys_[id].irq.destroy(); |
| } |
| thrd_join(irq_thread_, NULL); |
| } |
| |
| void Mt8167I2c::DdkUnbind(ddk::UnbindTxn txn) { |
| ShutDown(); |
| txn.Reply(); |
| } |
| |
| void Mt8167I2c::DdkRelease() { delete this; } |
| |
| int Mt8167I2c::TestThread() { |
| #ifdef TEST_USB_REGS_READ |
| constexpr uint32_t bus_id = 2; |
| constexpr uint8_t addr = 0x48; |
| Reset(bus_id); |
| for (uint8_t data_write = 0; data_write < 0xF; ++data_write) { |
| uint8_t data_read; |
| i2c_impl_op_t ops[] = { |
| {.address = addr, |
| .data_buffer = &data_write, |
| .data_size = 1, |
| .is_read = false, |
| .stop = false}, |
| {.address = addr, .data_buffer = &data_read, .data_size = 1, .is_read = true, .stop = true}, |
| }; |
| auto status = I2cImplTransact(bus_id, ops, countof(ops)); |
| if (status == ZX_OK) { |
| zxlogf(INFO, "I2C Addr: 0x%02X Reg:0x%02X Value:0x%02X", addr, data_write, data_read); |
| } |
| } |
| #endif |
| return 0; |
| } |
| |
| zx_status_t Mt8167I2c::GetI2cGpios(fbl::Array<ddk::GpioProtocolClient>* out) { |
| constexpr size_t kGpioCount = 6; |
| fbl::AllocChecker ac; |
| fbl::Array<ddk::GpioProtocolClient> gpios(new (&ac) ddk::GpioProtocolClient[kGpioCount], |
| kGpioCount); |
| if (!ac.check()) { |
| zxlogf(ERROR, "%s ZX_ERR_NO_MEMORY", __FUNCTION__); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| gpios[0] = ddk::GpioProtocolClient(parent(), "gpio-sda-0"); |
| gpios[1] = ddk::GpioProtocolClient(parent(), "gpio-scl-0"); |
| gpios[2] = ddk::GpioProtocolClient(parent(), "gpio-sda-1"); |
| gpios[3] = ddk::GpioProtocolClient(parent(), "gpio-scl-1"); |
| gpios[4] = ddk::GpioProtocolClient(parent(), "gpio-sda-2"); |
| gpios[5] = ddk::GpioProtocolClient(parent(), "gpio-scl-2"); |
| for (uint32_t i = 0; i < kGpioCount; i++) { |
| if (!gpios[i].is_valid()) { |
| zxlogf(ERROR, "%s failed to get gpio fragment", __FUNCTION__); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| } |
| |
| *out = std::move(gpios); |
| return ZX_OK; |
| } |
| |
| zx_status_t Mt8167I2c::DoDummyTransactions() { |
| fbl::Array<ddk::GpioProtocolClient> gpios; |
| zx_status_t status = GetI2cGpios(&gpios); |
| if (status != ZX_OK || gpios.size() == 0) { |
| return status; |
| } |
| |
| for (const ddk::GpioProtocolClient& gpio : gpios) { |
| gpio.SetAltFunction(kAltFunctionGpio); |
| } |
| |
| // Do one dummy write on each bus. This works around an issue where the first transaction after |
| // enabling the VGP1 regulator gets a NACK error. |
| // TODO(fxbug.dev/33282): Figure out a fix for this instead of working around it. |
| for (uint32_t id = 0; id < bus_count_; id++) { |
| uint8_t byte = 0; |
| i2c_impl_op_t ops = {.address = 0x00, |
| .data_buffer = &byte, |
| .data_size = sizeof(byte), |
| .is_read = false, |
| .stop = true}; |
| |
| I2cImplTransact(id, &ops, 1); |
| Reset(id); |
| } |
| |
| for (const ddk::GpioProtocolClient& gpio : gpios) { |
| gpio.SetAltFunction(kAltFunctionI2c); |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Mt8167I2c::Bind() { |
| zx_status_t status; |
| |
| status = zx::port::create(ZX_PORT_BIND_TO_INTERRUPT, &irq_port_); |
| if (status != ZX_OK) { |
| return status; |
| } |
| auto pdev = ddk::PDev::FromFragment(parent()); |
| pdev_device_info_t info; |
| status = pdev.GetDeviceInfo(&info); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s pdev_get_device_info failed %d", __FUNCTION__, status); |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| bus_count_ = info.mmio_count - 1; // Last MMIO is for XO clock. |
| if (bus_count_ != MT8167_I2C_CNT) { |
| zxlogf(ERROR, "%s wrong I2C count %d", __FUNCTION__, bus_count_); |
| return ZX_ERR_INTERNAL; |
| } |
| |
| std::optional<ddk::MmioBuffer> mmio; |
| status = pdev.MapMmio(bus_count_, &mmio); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s MapMmio %u failed %d", __FUNCTION__, bus_count_, status); |
| return status; |
| } |
| xo_regs_ = XoRegs(std::move(*mmio)); // Last MMIO is for XO clock. |
| |
| for (uint32_t id = 0; id < bus_count_; id++) { |
| status = pdev.MapMmio(id, &mmio); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s MapMmio %d failed %d", __FUNCTION__, id, status); |
| return status; |
| } |
| |
| zx::event event; |
| status = zx::event::create(0, &event); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s zx::event::create failed %d", __FUNCTION__, status); |
| return status; |
| } |
| keys_.push_back({std::move(*mmio), zx::interrupt(), std::move(event)}); |
| |
| status = pdev.GetInterrupt(id, &keys_[id].irq); |
| if (status != ZX_OK) { |
| return status; |
| } |
| status = keys_[id].irq.bind(irq_port_, id, 0); // id is the port key used. |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // TODO(andresoportus): Add support for turn on only during transactions?. |
| xo_regs_.value().ClockEnable(id, true); |
| |
| // TODO(andresoportus): Add support for DMA mode. |
| } |
| |
| auto thunk = [](void* arg) -> int { return reinterpret_cast<Mt8167I2c*>(arg)->IrqThread(); }; |
| int rc = thrd_create_with_name(&irq_thread_, thunk, this, "mt8167-i2c"); |
| if (rc != thrd_success) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| status = DoDummyTransactions(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| bind_finished_ = true; |
| |
| status = DdkAdd("mt8167-i2c"); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s DdkAdd failed: %d", __FUNCTION__, status); |
| ShutDown(); |
| } |
| return status; |
| } |
| |
| zx_status_t Mt8167I2c::Init() { |
| auto cleanup = fbl::MakeAutoCall([&]() { ShutDown(); }); |
| |
| #ifdef TEST_USB_REGS_READ |
| auto thunk = [](void* arg) -> int { return reinterpret_cast<Mt8167I2c*>(arg)->TestThread(); }; |
| int rc = thrd_create_with_name(&irq_thread_, thunk, this, "mt8167-i2c-test"); |
| if (rc != thrd_success) { |
| return ZX_ERR_INTERNAL; |
| } |
| #endif |
| |
| cleanup.cancel(); |
| return ZX_OK; |
| } |
| |
| zx_status_t Mt8167I2c::Create(void* ctx, zx_device_t* parent) { |
| fbl::AllocChecker ac; |
| auto dev = fbl::make_unique_checked<Mt8167I2c>(&ac, parent); |
| if (!ac.check()) { |
| zxlogf(ERROR, "%s ZX_ERR_NO_MEMORY", __FUNCTION__); |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| auto status = dev->Bind(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // devmgr is now in charge of the memory for dev |
| auto ptr = dev.release(); |
| |
| return ptr->Init(); |
| } |
| |
| static constexpr zx_driver_ops_t driver_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = Mt8167I2c::Create; |
| return ops; |
| }(); |
| |
| } // namespace mt8167_i2c |
| |
| ZIRCON_DRIVER(mt8167_i2c, mt8167_i2c::driver_ops, "zircon", "0.1"); |