| // 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 "qcom-gpio.h" |
| |
| #include <algorithm> |
| |
| #include <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/device.h> |
| #include <fbl/alloc_checker.h> |
| #include <fbl/array.h> |
| |
| namespace { |
| |
| uint64_t kPortKeyIrqMsg = 0x00; |
| uint64_t kPortKeyTerminate = 0x01; |
| |
| } // namespace |
| |
| namespace gpio { |
| |
| int QcomGpioDevice::Thread() { |
| while (1) { |
| zx_port_packet_t packet; |
| auto status = port_.wait(zx::time::infinite(), &packet); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s port wait failed: %d", __func__, status); |
| return thrd_error; |
| } |
| zxlogf(DEBUG, "%s msg on port key %lu", __func__, packet.key); |
| if (packet.key == kPortKeyTerminate) { |
| zxlogf(INFO, "QCOM GPIO thread terminating"); |
| return 0; |
| } |
| size_t index = 0; |
| status = enabled_ints_cache_.Find(true, 0, kGpioMax, 1, &index); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s no interrupt found in cache %d", __func__, status); |
| } |
| while (status == ZX_OK) { |
| zxlogf(DEBUG, "%s msg on port INT %lu", __func__, index); |
| if (status_int_.Status(index)) { |
| status = interrupts_[index].trigger(0, zx::time(packet.interrupt.timestamp)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s zx_interrupt_trigger failed %d", __func__, status); |
| } |
| status_int_.Clear(index); |
| } else { |
| zxlogf(ERROR, "%s interrupt %lu not enabled in reg", __func__, index); |
| } |
| status = enabled_ints_cache_.Find(true, index + 1, kGpioMax, 1, &index); |
| if (status != ZX_ERR_NO_RESOURCES) { // not just not-found in cache. |
| zxlogf(ERROR, "%s error in reading from cache %d", __func__, status); |
| } |
| } |
| combined_int_.ack(); |
| } |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplConfigIn(uint32_t index, uint32_t flags) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| GpioCfgReg::SetMode(&gpio_mmio_, index, GpioCfgReg::kModeGpio); |
| GpioCfgReg::SetOut(&gpio_mmio_, index, false); |
| const uint32_t pull_mode = flags & GPIO_PULL_MASK; |
| |
| // clang-format off |
| switch (pull_mode) { |
| case GPIO_NO_PULL: GpioCfgReg::SetPullNone(&gpio_mmio_, index); break; |
| case GPIO_PULL_DOWN: GpioCfgReg::SetPullDown(&gpio_mmio_, index); break; |
| case GPIO_PULL_UP: GpioCfgReg::SetPullUp (&gpio_mmio_, index); break; |
| default: return ZX_ERR_NOT_SUPPORTED; |
| } |
| // clang-format on |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplConfigOut(uint32_t index, uint8_t initial_value) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| GpioCfgReg::SetMode(&gpio_mmio_, index, GpioCfgReg::kModeGpio); |
| GpioCfgReg::SetOut(&gpio_mmio_, index, true); |
| return GpioImplWrite(index, initial_value); |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplSetAltFunction(uint32_t index, uint64_t function) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| if (function >= GpioCfgReg::kModeMax) { |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| GpioCfgReg::SetMode(&gpio_mmio_, index, static_cast<uint32_t>(function)); |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplSetDriveStrength(uint32_t index, uint64_t ua, |
| uint64_t* out_actual_ua) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| uint64_t supported_uas[] = {2000, 4000, 6000, 8000, 10000, 12000, 14000, 16000}; |
| if (std::find(std::begin(supported_uas), std::end(supported_uas), ua) == |
| std::end(supported_uas)) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| GpioCfgReg::SetStrength(&gpio_mmio_, index, static_cast<uint8_t>(ua / 1000)); |
| if (out_actual_ua) { |
| *out_actual_ua = ua; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplRead(uint32_t index, uint8_t* out_value) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| *out_value = static_cast<uint8_t>(in_out_.GetVal(index)); |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplWrite(uint32_t index, uint8_t value) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| in_out_.SetVal(index, value); |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplGetInterrupt(uint32_t index, uint32_t flags, |
| zx::interrupt* out_irq) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| |
| zx::interrupt irq; |
| auto status = zx::interrupt::create(zx::resource(), index, ZX_INTERRUPT_VIRTUAL, &irq); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s zx::interrupt::create failed %d", __func__, status); |
| return status; |
| } |
| status = irq.duplicate(ZX_RIGHT_SAME_RIGHTS, out_irq); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s interrupt.duplicate failed %d", __func__, status); |
| return status; |
| } |
| |
| // clang-format off |
| switch (flags & ZX_INTERRUPT_MODE_MASK) { |
| case ZX_INTERRUPT_MODE_EDGE_LOW: int_cfg_.SetMode(index, Mode::EdgeLow); break; |
| case ZX_INTERRUPT_MODE_EDGE_HIGH: int_cfg_.SetMode(index, Mode::EdgeHigh); break; |
| case ZX_INTERRUPT_MODE_LEVEL_LOW: int_cfg_.SetMode(index, Mode::LevelLow); break; |
| case ZX_INTERRUPT_MODE_LEVEL_HIGH: int_cfg_.SetMode(index, Mode::LevelHigh); break; |
| case ZX_INTERRUPT_MODE_EDGE_BOTH: int_cfg_.SetMode(index, Mode::EdgeDual); break; |
| default: return ZX_ERR_INVALID_ARGS; |
| } |
| // clang-format on |
| |
| interrupts_[index] = std::move(irq); |
| // TODO(andresoportus): Once we define which cases would use them, enable direct interrupts |
| // (via TlmmDirConnIntReg). |
| status_int_.Clear(index); |
| int_cfg_.EnableCombined(index, true); |
| enabled_ints_cache_.SetOne(index); |
| zxlogf(DEBUG, "%s INT %u enabled", __func__, index); |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplReleaseInterrupt(uint32_t index) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| interrupts_[index].destroy(); |
| interrupts_[index].reset(); |
| int_cfg_.EnableCombined(index, false); |
| enabled_ints_cache_.ClearOne(index); |
| zxlogf(DEBUG, "%s INT %u disabled", __func__, index); |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::GpioImplSetPolarity(uint32_t index, uint32_t polarity) { |
| if (index >= kGpioMax) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| int_cfg_.SetPolarity(index, static_cast<bool>(polarity)); |
| return ZX_OK; |
| } |
| |
| void QcomGpioDevice::ShutDown() { |
| combined_int_.destroy(); |
| zx_port_packet packet = {kPortKeyTerminate, ZX_PKT_TYPE_USER, ZX_OK, {}}; |
| auto status = port_.queue(&packet); |
| ZX_ASSERT(status == ZX_OK); |
| thrd_join(thread_, NULL); |
| } |
| |
| void QcomGpioDevice::DdkUnbind(ddk::UnbindTxn txn) { |
| ShutDown(); |
| txn.Reply(); |
| } |
| |
| void QcomGpioDevice::DdkRelease() { delete this; } |
| |
| zx_status_t QcomGpioDevice::Bind() { |
| auto status = pdev_.GetInterrupt(0, &combined_int_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s GetInterrupt failed %d", __func__, status); |
| return status; |
| } |
| |
| status = zx::port::create(ZX_PORT_BIND_TO_INTERRUPT, &port_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s port create failed %d", __func__, status); |
| return status; |
| } |
| |
| status = combined_int_.bind(port_, kPortKeyIrqMsg, 0 /*options*/); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s interrupt bind failed %d", __func__, status); |
| return status; |
| } |
| |
| fbl::AllocChecker ac; |
| interrupts_ = fbl::Array(new (&ac) zx::interrupt[kGpioMax], kGpioMax); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| auto cb = [](void* arg) -> int { return reinterpret_cast<QcomGpioDevice*>(arg)->Thread(); }; |
| int rc = thrd_create_with_name(&thread_, cb, this, "qcom-gpio-thread"); |
| if (rc != thrd_success) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| status = DdkAdd("qcom-gpio"); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s DdkAdd failed %d", __func__, status); |
| ShutDown(); |
| return status; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t QcomGpioDevice::Init() { |
| pbus_protocol_t pbus; |
| auto status = device_get_protocol(parent(), ZX_PROTOCOL_PBUS, &pbus); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s: ZX_PROTOCOL_PBUS not available %d", __func__, status); |
| return status; |
| } |
| gpio_impl_protocol_t gpio_proto = { |
| .ops = &gpio_impl_protocol_ops_, |
| .ctx = this, |
| }; |
| status = pbus_register_protocol(&pbus, ZX_PROTOCOL_GPIO_IMPL, &gpio_proto, sizeof(gpio_proto)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s pbus_register_protocol failed %d", __func__, status); |
| ShutDown(); |
| return status; |
| } |
| return enabled_ints_cache_.Reset(kGpioMax); // Clear and resize. |
| } |
| |
| zx_status_t QcomGpioDevice::Create(zx_device_t* parent) { |
| pdev_protocol_t pdev; |
| zx_status_t status = device_get_protocol(parent, ZX_PROTOCOL_PDEV, &pdev); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s ZX_PROTOCOL_PDEV not available %d", __func__, status); |
| return status; |
| } |
| |
| mmio_buffer_t gpio_mmio; |
| status = pdev_map_mmio_buffer(&pdev, 0, ZX_CACHE_POLICY_UNCACHED_DEVICE, &gpio_mmio); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s gpio pdev_map_mmio_buffer failed %d", __func__, status); |
| return status; |
| } |
| |
| fbl::AllocChecker ac; |
| auto dev = |
| fbl::make_unique_checked<gpio::QcomGpioDevice>(&ac, parent, ddk::MmioBuffer(gpio_mmio)); |
| if (!ac.check()) { |
| zxlogf(ERROR, "qcom_gpio_bind: ZX_ERR_NO_MEMORY"); |
| return ZX_ERR_NO_MEMORY; |
| } |
| 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(); |
| } |
| |
| zx_status_t qcom_gpio_bind(void* ctx, zx_device_t* parent) { |
| return gpio::QcomGpioDevice::Create(parent); |
| } |
| |
| } // namespace gpio |