| // 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 "gt92xx.h" |
| |
| #include <endian.h> |
| #include <lib/ddk/binding_driver.h> |
| #include <lib/ddk/debug.h> |
| #include <lib/ddk/platform-defs.h> |
| #include <lib/ddk/trace/event.h> |
| #include <lib/fit/defer.h> |
| #include <lib/zx/clock.h> |
| #include <lib/zx/profile.h> |
| #include <lib/zx/thread.h> |
| #include <lib/zx/time.h> |
| #include <zircon/status.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/threads.h> |
| |
| #include <utility> |
| |
| #include <fbl/algorithm.h> |
| #include <fbl/auto_lock.h> |
| #include <fbl/vector.h> |
| |
| namespace goodix { |
| |
| namespace { |
| |
| constexpr fuchsia_input_report::wire::Axis XYAxis(int64_t max) { |
| return { |
| .range = {.min = 0, .max = max}, |
| .unit = |
| { |
| .type = fuchsia_input_report::wire::UnitType::kOther, |
| .exponent = 0, |
| }, |
| }; |
| } |
| |
| constexpr size_t kXMax = 600; |
| constexpr size_t kYMax = 1024; |
| |
| } // namespace |
| |
| void Gt92xxDevice::GtInputReport::ToFidlInputReport( |
| fidl::WireTableBuilder<::fuchsia_input_report::wire::InputReport>& input_report, |
| fidl::AnyArena& allocator) { |
| fidl::VectorView<fuchsia_input_report::wire::ContactInputReport> contact_rpt(allocator, |
| contact_count); |
| for (size_t i = 0; i < contact_count; i++) { |
| contact_rpt[i] = fuchsia_input_report::wire::ContactInputReport::Builder(allocator) |
| .contact_id(contacts[i].finger_id) |
| .position_x(contacts[i].x) |
| .position_y(contacts[i].y) |
| .Build(); |
| } |
| |
| auto touch_report = |
| fuchsia_input_report::wire::TouchInputReport::Builder(allocator).contacts(contact_rpt); |
| input_report.event_time(event_time.get()).touch(touch_report.Build()); |
| } |
| |
| // clang-format off |
| // Configuration data |
| // first two bytes contain starting register address (part of i2c transaction) |
| fbl::Vector<uint8_t> Gt92xxDevice::GetConfData() { |
| return {GT_REG_CONFIG_DATA >> 8, |
| GT_REG_CONFIG_DATA & 0xff, |
| 0x5f, 0x00, 0x04, 0x58, 0x02, 0x05, 0xbd, 0xc0, |
| 0x00, 0x08, 0x1e, 0x05, 0x50, 0x32, 0x00, 0x0b, |
| 0x00, 0x00, 0x00, 0x00, 0x40, 0x12, 0x00, 0x17, |
| 0x17, 0x19, 0x12, 0x8d, 0x2d, 0x0f, 0x3f, 0x41, |
| 0xb2, 0x04, 0x00, 0x00, 0x00, 0xbc, 0x03, 0x1d, |
| 0x1e, 0x80, 0x01, 0x00, 0x14, 0x46, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x37, 0x55, 0x8f, 0xc5, 0x02, |
| 0x07, 0x11, 0x00, 0x04, 0x8a, 0x39, 0x00, 0x81, |
| 0x3e, 0x00, 0x78, 0x44, 0x00, 0x71, 0x4a, 0x00, |
| 0x6a, 0x51, 0x00, 0x6a, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, |
| 0x1c, 0x1a, 0x18, 0x16, 0x14, 0x12, 0x10, 0x0e, |
| 0x0c, 0x0a, 0x08, 0x06, 0x04, 0x02, 0x00, 0x00, |
| 0xff, 0xff, 0x1f, 0xe7, 0xff, 0xff, 0xff, 0x0f, |
| 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x2a, 0x29, |
| 0x28, 0x27, 0x26, 0x25, 0x24, 0x23, 0x22, 0x21, |
| 0x20, 0x1f, 0x1e, 0x0c, 0x0b, 0x0a, 0x09, 0x08, |
| 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, |
| 0x00, 0x00, 0x6e, 0x01 }; |
| } |
| // clang-format on |
| |
| int Gt92xxDevice::Thread() { |
| // Format of data as it is read from the device |
| struct FingerReport { |
| uint8_t id; |
| uint16_t x; |
| uint16_t y; |
| uint16_t size; |
| uint8_t reserved; |
| } __PACKED; |
| |
| zx_status_t status; |
| zx::time timestamp; |
| zxlogf(INFO, "gt92xx: entering irq thread"); |
| while (true) { |
| status = irq_.wait(×tamp); |
| if (!running_.load()) { |
| return ZX_OK; |
| } |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "gt92xx: Interrupt error %d", status); |
| } |
| TRACE_DURATION("input", "Gt92xxDevice Read"); |
| uint8_t touch_stat = 0; |
| uint8_t retry_cnt = 0; |
| // Datasheet implies that it is not guaranteed that report will be |
| // ready when interrupt is generated, so allow a couple retries to check |
| // touch status. |
| while (!(touch_stat & GT_REG_TOUCH_STATUS_READY) && (retry_cnt < 3)) { |
| zx::result<uint8_t> read_status = Read(GT_REG_TOUCH_STATUS); |
| if (read_status.is_ok()) { |
| touch_stat = read_status.value(); |
| } |
| if (!(touch_stat & GT_REG_TOUCH_STATUS_READY)) { |
| retry_cnt++; |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(1))); |
| } |
| } |
| |
| if (touch_stat & GT_REG_TOUCH_STATUS_READY) { |
| uint8_t num_reports = touch_stat & 0x0f; |
| FingerReport reports[kMaxPoints]; |
| // Read touch reports |
| zx_status_t status = Read(GT_REG_REPORTS, reinterpret_cast<uint8_t*>(&reports), |
| static_cast<uint8_t>(sizeof(FingerReport) * kMaxPoints)); |
| // Clear touch status after reading reports |
| Write(GT_REG_TOUCH_STATUS, 0); |
| if (status == ZX_OK) { |
| GtInputReport report; |
| report.event_time = timestamp; |
| report.contact_count = |
| std::min(num_reports, static_cast<uint8_t>(report.contacts.max_size())); |
| // We are reusing same HID report as ft3x77 to simplify astro integration |
| // so we need to copy from device format to HID structure format |
| for (uint32_t i = 0; i < report.contact_count; i++) { |
| report.contacts[i].finger_id = reports[i].id; |
| report.contacts[i].y = reports[i].x; |
| report.contacts[i].x = reports[i].y; |
| } |
| readers_.SendReportToAllReaders(report); |
| |
| const zx::duration latency = zx::clock::get_monotonic() - timestamp; |
| |
| total_latency_ += latency; |
| report_count_++; |
| average_latency_usecs_.Set(total_latency_.to_usecs() / report_count_); |
| |
| if (latency > max_latency_) { |
| max_latency_ = latency; |
| max_latency_usecs_.Set(max_latency_.to_usecs()); |
| } |
| |
| if (num_reports > 0) { |
| total_report_count_.Add(1); |
| last_event_timestamp_.Set(timestamp.get()); |
| } |
| } |
| } else { |
| zxlogf(ERROR, "gt92xx: Errant interrupt, no report ready - %x", touch_stat); |
| } |
| } |
| zxlogf(INFO, "gt92xx: exiting"); |
| return 0; |
| } |
| |
| zx_status_t Gt92xxDevice::Create(zx_device_t* device) { |
| zxlogf(INFO, "gt92xx: driver started..."); |
| ddk::I2cChannel i2c(device, "i2c"); |
| if (!i2c.is_valid()) { |
| zxlogf(ERROR, "failed to acquire i2c"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| const char* kInterruptGpioFragmentName = "gpio-int"; |
| zx::result int_gpio = DdkConnectFragmentFidlProtocol<fuchsia_hardware_gpio::Service::Device>( |
| device, kInterruptGpioFragmentName); |
| if (int_gpio.is_error()) { |
| zxlogf(ERROR, "Failed to get gpio protocol from fragment %s: %s", kInterruptGpioFragmentName, |
| int_gpio.status_string()); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| const char* kResetGpioFragmentName = "gpio-reset"; |
| zx::result reset_gpio = DdkConnectFragmentFidlProtocol<fuchsia_hardware_gpio::Service::Device>( |
| device, kResetGpioFragmentName); |
| if (reset_gpio.is_error()) { |
| zxlogf(ERROR, "Failed to get gpio protocol from fragment %s: %s", kResetGpioFragmentName, |
| reset_gpio.status_string()); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| auto goodix_dev = std::make_unique<Gt92xxDevice>( |
| device, fdf_dispatcher_get_async_dispatcher(fdf_dispatcher_get_current_dispatcher()), |
| std::move(i2c), std::move(int_gpio.value()), std::move(reset_gpio.value())); |
| |
| zx_status_t status = goodix_dev->Init(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Could not initialize gt92xx hardware %d", status); |
| return status; |
| } |
| |
| auto thunk = [](void* arg) -> int { return reinterpret_cast<Gt92xxDevice*>(arg)->Thread(); }; |
| |
| auto cleanup = fit::defer([&]() { goodix_dev->ShutDown(); }); |
| |
| goodix_dev->running_.store(true); |
| int ret = thrd_create_with_name(&goodix_dev->thread_, thunk, goodix_dev.get(), "gt92xx-thread"); |
| ZX_DEBUG_ASSERT(ret == thrd_success); |
| |
| // Set scheduler role for device thread. |
| { |
| const char* role_name = "fuchsia.ui.input.drivers.goodix.gt92xx.device"; |
| status = |
| device_set_profile_by_role(goodix_dev->parent(), thrd_get_zx_handle(goodix_dev->thread_), |
| role_name, strlen(role_name)); |
| if (status != ZX_OK) { |
| zxlogf(WARNING, "Gt92xxDevice::Create: Failed to apply role: %s", |
| zx_status_get_string(status)); |
| } |
| } |
| |
| status = goodix_dev->DdkAdd(ddk::DeviceAddArgs("gt92xx-HidDevice") |
| .set_inspect_vmo(goodix_dev->inspector_.DuplicateVmo())); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "gt92xx: Could not create hid device: %d", status); |
| return status; |
| } else { |
| zxlogf(INFO, "gt92xx: Added hid device"); |
| } |
| |
| cleanup.cancel(); |
| |
| // device intentionally leaked as it is now held by DevMgr |
| [[maybe_unused]] auto ptr = goodix_dev.release(); |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Gt92xxDevice::Init() { |
| // Hardware reset |
| HWReset(); |
| |
| zx_status_t status = UpdateFirmwareIfNeeded(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // Get the config data |
| fbl::Vector<uint8_t> Conf(GetConfData()); |
| |
| // Configuration data should span specific set of registers |
| // last register has flag to latch in new configuration, second |
| // to last register holds checksum of register values. |
| // Note: first two bytes of conf_data hold the 16-bit register address where |
| // the write will start. |
| ZX_DEBUG_ASSERT((Conf.size() - sizeof(uint16_t)) == |
| (GT_REG_CONFIG_REFRESH - GT_REG_CONFIG_DATA + 1)); |
| |
| zx::result<uint8_t> version = Read(GT_REG_CONFIG_DATA); |
| if (version.is_ok() && version.value() > Conf.data()[sizeof(uint16_t)]) { |
| // Force the controller to take the config. |
| Conf[sizeof(uint16_t)] = 0x00; |
| } |
| |
| // Write conf data to registers |
| status = Write(Conf.data(), Conf.size()); |
| if (status != ZX_OK) { |
| return status; |
| } |
| // Device requires 10ms delay to refresh configuration |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(10))); |
| // Clear touch state in case there were spurious touches registered |
| // during startup |
| Write(GT_REG_TOUCH_STATUS, 0); |
| |
| // Note: Our configuration inverts polarity of interrupt |
| // (datasheet implies it is active high) |
| fidl::WireResult interrupt_result = int_gpio_->GetInterrupt(ZX_INTERRUPT_MODE_EDGE_LOW); |
| if (!interrupt_result.ok()) { |
| zxlogf(ERROR, "Failed to send GetInterrupt request to interrupt gpio: %s", |
| interrupt_result.status_string()); |
| return interrupt_result.status(); |
| } |
| if (interrupt_result->is_error()) { |
| zxlogf(ERROR, "Failed to get interrupt from interrupt gpio: %s", |
| zx_status_get_string(interrupt_result->error_value())); |
| return interrupt_result->error_value(); |
| } |
| irq_ = std::move(interrupt_result.value()->irq); |
| |
| LogFirmwareStatus(); |
| |
| metrics_root_ = inspector_.GetRoot().CreateChild("hid-input-report-touch"); |
| average_latency_usecs_ = metrics_root_.CreateUint("average_latency_usecs", 0); |
| max_latency_usecs_ = metrics_root_.CreateUint("max_latency_usecs", 0); |
| total_report_count_ = metrics_root_.CreateUint("total_report_count", 0); |
| last_event_timestamp_ = metrics_root_.CreateUint("last_event_timestamp", 0); |
| return ZX_OK; |
| } |
| |
| zx_status_t Gt92xxDevice::HWReset() { |
| // Hardware reset will also set the address of the controller to either |
| // 0x14 0r 0x5d. See the datasheet for explanation of sequence. |
| { |
| fidl::WireResult result = reset_gpio_->ConfigOut(0); // Make reset pin an output and pull low |
| if (!result.ok()) { |
| zxlogf(ERROR, "Failed to send ConfigOut request to reset gpio: %s", result.status_string()); |
| return result.status(); |
| } |
| if (result->is_error()) { |
| zxlogf(ERROR, "Failed to configure reset gpio to output: %s", |
| zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| } |
| |
| { |
| fidl::WireResult result = int_gpio_->ConfigOut(0); // Make interrupt pin an output and pull low |
| if (!result.ok()) { |
| zxlogf(ERROR, "Failed to send ConfigOut request to interrupt gpio: %s", |
| result.status_string()); |
| return result.status(); |
| } |
| if (result->is_error()) { |
| zxlogf(ERROR, "Failed to configure interrupt gpio to output: %s", |
| zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| } |
| |
| // Delay for 100us |
| zx_nanosleep(zx_deadline_after(ZX_USEC(100))); |
| { |
| fidl::WireResult result = reset_gpio_->Write(1); // Release the reset |
| if (!result.ok()) { |
| zxlogf(ERROR, "Failed to send Write request to reset gpio: %s", result.status_string()); |
| return result.status(); |
| } |
| if (result->is_error()) { |
| zxlogf(ERROR, "Failed to write to reset gpio: %s", |
| zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| } |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(5 + 50))); // Interrupt still output low |
| { |
| fidl::WireResult result = int_gpio_->ConfigIn( |
| fuchsia_hardware_gpio::GpioFlags::kPullUp); // Make interrupt pin an input again |
| if (!result.ok()) { |
| zxlogf(ERROR, "Failed to send ConfigIn request to interrupt gpio: %s", |
| result.status_string()); |
| return result.status(); |
| } |
| if (result->is_error()) { |
| zxlogf(ERROR, "Failed to configure interrupt gpio to input: %s", |
| zx_status_get_string(result->error_value())); |
| return result->error_value(); |
| } |
| } |
| zx_nanosleep(zx_deadline_after(ZX_MSEC(50))); // Wait for reset before sending config |
| return ZX_OK; |
| } |
| |
| void Gt92xxDevice::DdkRelease() { delete this; } |
| |
| void Gt92xxDevice::DdkUnbind(ddk::UnbindTxn txn) { |
| ShutDown(); |
| txn.Reply(); |
| } |
| |
| zx_status_t Gt92xxDevice::ShutDown() { |
| running_.store(false); |
| irq_.destroy(); |
| thrd_join(thread_, NULL); |
| return ZX_OK; |
| } |
| |
| void Gt92xxDevice::GetInputReportsReader(GetInputReportsReaderRequestView request, |
| GetInputReportsReaderCompleter::Sync& completer) { |
| auto status = readers_.CreateReader(dispatcher_, std::move(request->reader)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Failed to create a reader %d", status); |
| } |
| } |
| |
| void Gt92xxDevice::GetDescriptor(GetDescriptorCompleter::Sync& completer) { |
| fidl::Arena<kFeatureAndDescriptorBufferSize> allocator; |
| |
| fuchsia_input_report::wire::DeviceInfo device_info; |
| device_info.vendor_id = static_cast<uint32_t>(fuchsia_input_report::wire::VendorId::kGoogle); |
| device_info.product_id = |
| static_cast<uint32_t>(fuchsia_input_report::wire::VendorGoogleProductId::kGoodixTouchscreen); |
| |
| fidl::VectorView<fuchsia_input_report::wire::ContactInputDescriptor> contacts(allocator, |
| kMaxPoints); |
| for (auto& c : contacts) { |
| c = fuchsia_input_report::wire::ContactInputDescriptor::Builder(allocator) |
| .position_x(XYAxis(kXMax)) |
| .position_y(XYAxis(kYMax)) |
| .Build(); |
| } |
| |
| const auto input = fuchsia_input_report::wire::TouchInputDescriptor::Builder(allocator) |
| .touch_type(fuchsia_input_report::wire::TouchType::kTouchscreen) |
| .max_contacts(kMaxPoints) |
| .contacts(contacts) |
| .Build(); |
| |
| const auto touch = |
| fuchsia_input_report::wire::TouchDescriptor::Builder(allocator).input(input).Build(); |
| |
| const auto descriptor = fuchsia_input_report::wire::DeviceDescriptor::Builder(allocator) |
| .device_info(device_info) |
| .touch(touch) |
| .Build(); |
| |
| completer.Reply(descriptor); |
| } |
| |
| zx::result<uint8_t> Gt92xxDevice::Read(uint16_t addr) { |
| uint8_t rbuf; |
| zx_status_t status = Read(addr, &rbuf, 1); |
| if (status != ZX_OK) { |
| return zx::error(status); |
| } |
| return zx::ok(rbuf); |
| } |
| |
| zx_status_t Gt92xxDevice::Read(uint16_t addr, uint8_t* buf, size_t len) { |
| zx_status_t status; |
| for (int i = 0; i < kI2cRetries; i++) { |
| uint8_t tbuf[2]; |
| tbuf[0] = static_cast<uint8_t>(addr >> 8); |
| tbuf[1] = static_cast<uint8_t>(addr & 0xff); |
| if ((status = i2c_.WriteReadSync(tbuf, 2, buf, len)) == ZX_OK) { |
| return ZX_OK; |
| } |
| } |
| zxlogf(ERROR, "Failed to read %s 0x%04x: %d", len <= 1 ? "register" : "from", addr, status); |
| return status; |
| } |
| |
| zx_status_t Gt92xxDevice::Write(uint16_t addr, uint8_t val) { |
| zx_status_t status; |
| for (int i = 0; i < kI2cRetries; i++) { |
| uint8_t tbuf[3]; |
| tbuf[0] = static_cast<uint8_t>(addr >> 8); |
| tbuf[1] = static_cast<uint8_t>(addr & 0xff); |
| tbuf[2] = val; |
| if ((status = i2c_.WriteSync(tbuf, 3)) == ZX_OK) { |
| return ZX_OK; |
| } |
| } |
| zxlogf(ERROR, "Failed to write register 0x%04x: %d", addr, status); |
| return status; |
| } |
| |
| zx_status_t Gt92xxDevice::Write(uint8_t* buf, size_t len) { |
| zx_status_t status; |
| for (int i = 0; i < kI2cRetries; i++) { |
| if ((status = i2c_.WriteSync(buf, len)) == ZX_OK) { |
| return ZX_OK; |
| } |
| } |
| zxlogf(ERROR, "Failed to write to 0x%04x: %d", (buf[0] << 8) | buf[1], status); |
| return status; |
| } |
| |
| } // namespace goodix |
| |
| zx_status_t gt92xx_bind(void* ctx, zx_device_t* device) { |
| return goodix::Gt92xxDevice::Create(device); |
| } |
| |
| static constexpr zx_driver_ops_t gt92xx_driver_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = gt92xx_bind; |
| return ops; |
| }(); |
| |
| ZIRCON_DRIVER(gt92xx, gt92xx_driver_ops, "zircon", "0.1"); |