| // 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 "tcs3400.h" |
| |
| #include <fuchsia/input/report/llcpp/fidl.h> |
| #include <lib/device-protocol/i2c.h> |
| #include <string.h> |
| #include <threads.h> |
| #include <unistd.h> |
| #include <zircon/syscalls.h> |
| #include <zircon/syscalls/port.h> |
| |
| #include <algorithm> |
| |
| #include <ddk/binding.h> |
| #include <ddk/debug.h> |
| #include <ddk/metadata.h> |
| #include <ddk/platform-defs.h> |
| #include <ddktl/metadata/light-sensor.h> |
| #include <ddktl/protocol/composite.h> |
| #include <fbl/algorithm.h> |
| #include <fbl/auto_call.h> |
| #include <fbl/auto_lock.h> |
| #include <hid/descriptor.h> |
| |
| #include "tcs3400-regs.h" |
| |
| namespace { |
| constexpr zx_duration_t INTERRUPTS_HYSTERESIS = ZX_MSEC(100); |
| constexpr uint8_t SAMPLES_TO_TRIGGER = 0x01; |
| |
| // Repeat saturated log line every two minutes |
| constexpr uint16_t kSaturatedLogTimeSecs = 120; |
| // Bright, not saturated values to return when saturated |
| constexpr uint16_t kMaxSaturationRed = 21'067; |
| constexpr uint16_t kMaxSaturationGreen = 20'395; |
| constexpr uint16_t kMaxSaturationBlue = 20'939; |
| constexpr uint16_t kMaxSaturationClear = 65'085; |
| |
| #define GET_BYTE(val, shift) static_cast<uint8_t>((val >> shift) & 0xFF) |
| |
| // TODO(fxbug.dev/37765): -Waddress-of-packed-member warns on pointers to members of |
| // packed structs because those pointers could be misaligned. The warning |
| // however can appear on areas where we just copy the value of the pointer |
| // instead of access it. These macros silence it by casting to a void* and back. |
| #define UNALIGNED_U16_PTR(val) (uint16_t*)((void*)val) |
| |
| // clang-format off |
| // zx_port_packet::type |
| #define TCS_SHUTDOWN 0x01 |
| #define TCS_CONFIGURE 0x02 |
| #define TCS_INTERRUPT 0x03 |
| #define TCS_REARM_IRQ 0x04 |
| #define TCS_POLL 0x05 |
| // clang-format on |
| |
| } // namespace |
| |
| namespace tcs { |
| |
| zx_status_t Tcs3400Device::FillInputRpt() { |
| bool saturatedReading = false; |
| input_rpt_.rpt_id = AMBIENT_LIGHT_RPT_ID_INPUT; |
| struct Regs { |
| uint16_t* out; |
| uint8_t reg_h; |
| uint8_t reg_l; |
| } regs[] = { |
| {UNALIGNED_U16_PTR(&input_rpt_.illuminance), TCS_I2C_CDATAH, TCS_I2C_CDATAL}, |
| {UNALIGNED_U16_PTR(&input_rpt_.red), TCS_I2C_RDATAH, TCS_I2C_RDATAL}, |
| {UNALIGNED_U16_PTR(&input_rpt_.green), TCS_I2C_GDATAH, TCS_I2C_GDATAL}, |
| {UNALIGNED_U16_PTR(&input_rpt_.blue), TCS_I2C_BDATAH, TCS_I2C_BDATAL}, |
| }; |
| for (const auto& i : regs) { |
| uint8_t buf_h, buf_l; |
| zx_status_t status; |
| fbl::AutoLock lock(&i2c_lock_); |
| // Read lower byte first, the device holds upper byte of a sample in a shadow register after |
| // a lower byte read |
| status = i2c_.WriteReadSync(&i.reg_l, 1, &buf_l, 1); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Tcs3400Device::FillInputRpt: i2c_write_read_sync failed: %d", status); |
| input_rpt_.state = HID_USAGE_SENSOR_STATE_ERROR_VAL; |
| return status; |
| } |
| status = i2c_.WriteReadSync(&i.reg_h, 1, &buf_h, 1); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Tcs3400Device::FillInputRpt: i2c_write_read_sync failed: %d", status); |
| input_rpt_.state = HID_USAGE_SENSOR_STATE_ERROR_VAL; |
| return status; |
| } |
| auto out = static_cast<uint16_t>(static_cast<float>(((buf_h & 0xFF) << 8) | (buf_l & 0xFF))); |
| |
| // Use memcpy here because i.out is a misaligned pointer and dereferencing a |
| // misaligned pointer is UB. This ends up getting lowered to a 16-bit store. |
| memcpy(i.out, &out, sizeof(out)); |
| saturatedReading |= (out == 65'535); |
| |
| zxlogf(DEBUG, "raw: 0x%04X again: %u atime: %u", out, again_, atime_); |
| } |
| if (saturatedReading) { |
| // Saturated, ignoring the IR channel because we only looked at RGBC. |
| // Return very bright value so that consumers can adjust screens etc accordingly. |
| input_rpt_.red = kMaxSaturationRed; |
| input_rpt_.green = kMaxSaturationGreen; |
| input_rpt_.blue = kMaxSaturationBlue; |
| input_rpt_.illuminance = kMaxSaturationClear; |
| // log one message when saturation starts and then |
| if (!isSaturated_ || difftime(time(NULL), lastSaturatedLog_) >= kSaturatedLogTimeSecs) { |
| zxlogf(INFO, "Tcs3400Device::FillInputRpt: sensor is saturated"); |
| time(&lastSaturatedLog_); |
| } |
| } else { |
| if (isSaturated_) { |
| zxlogf(INFO, "Tcs3400Device::FillInputRpt: sensor is no longer saturated"); |
| } |
| } |
| isSaturated_ = saturatedReading; |
| input_rpt_.state = HID_USAGE_SENSOR_STATE_READY_VAL; |
| return ZX_OK; |
| } |
| |
| int Tcs3400Device::Thread() { |
| // Both polling and interrupts are supported simultaneously |
| zx_time_t poll_timeout = ZX_TIME_INFINITE; |
| zx_time_t irq_rearm_timeout = ZX_TIME_INFINITE; |
| while (1) { |
| zx_port_packet_t packet; |
| zx_time_t timeout = std::min(poll_timeout, irq_rearm_timeout); |
| zx_status_t status = port_.wait(zx::time(timeout), &packet); |
| if (status != ZX_OK && status != ZX_ERR_TIMED_OUT) { |
| zxlogf(ERROR, "Tcs3400Device::Thread: port wait failed: %d", status); |
| return thrd_error; |
| } |
| |
| if (status == ZX_ERR_TIMED_OUT) { |
| if (timeout == irq_rearm_timeout) { |
| packet.key = TCS_REARM_IRQ; |
| } else { |
| packet.key = TCS_POLL; |
| } |
| } |
| |
| uint16_t threshold_low; |
| uint16_t threshold_high; |
| |
| switch (packet.key) { |
| case TCS_SHUTDOWN: |
| zxlogf(INFO, "Tcs3400Device::Thread: shutting down"); |
| return thrd_success; |
| case TCS_CONFIGURE: { |
| fbl::AutoLock lock(&feature_lock_); |
| threshold_low = feature_rpt_.threshold_low; |
| threshold_high = feature_rpt_.threshold_high; |
| if (feature_rpt_.interval_ms == 0) { // per spec 0 is device's default |
| poll_timeout = ZX_TIME_INFINITE; // we define the default as no polling |
| } else { |
| poll_timeout = zx_deadline_after(ZX_MSEC(feature_rpt_.interval_ms)); |
| } |
| } |
| { |
| struct Setup { |
| uint8_t cmd; |
| uint8_t val; |
| } __PACKED setup[] = { |
| {TCS_I2C_ENABLE, |
| TCS_I2C_ENABLE_POWER_ON | TCS_I2C_ENABLE_ADC_ENABLE | TCS_I2C_ENABLE_INT_ENABLE}, |
| {TCS_I2C_AILTL, GET_BYTE(threshold_low, 0)}, |
| {TCS_I2C_AILTH, GET_BYTE(threshold_low, 8)}, |
| {TCS_I2C_AIHTL, GET_BYTE(threshold_high, 0)}, |
| {TCS_I2C_AIHTH, GET_BYTE(threshold_high, 8)}, |
| {TCS_I2C_PERS, SAMPLES_TO_TRIGGER}, |
| }; |
| for (const auto& i : setup) { |
| fbl::AutoLock lock(&i2c_lock_); |
| status = i2c_.WriteSync(&i.cmd, sizeof(setup[0])); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Tcs3400Device::Thread: i2c_write_sync failed: %d", status); |
| break; // do not exit thread, future transactions may succeed |
| } |
| } |
| } |
| break; |
| case TCS_INTERRUPT: |
| zx_interrupt_ack(irq_.get()); // rearm interrupt at the IRQ level |
| { |
| fbl::AutoLock lock(&feature_lock_); |
| threshold_low = feature_rpt_.threshold_low; |
| threshold_high = feature_rpt_.threshold_high; |
| } |
| { |
| fbl::AutoLock lock(&client_input_lock_); |
| if (FillInputRpt() == ZX_OK && client_.is_valid()) { |
| if (input_rpt_.illuminance > threshold_high) { |
| input_rpt_.event = HID_USAGE_SENSOR_EVENT_HIGH_THRESHOLD_CROSS_UPWARD_VAL; |
| client_.IoQueue(&input_rpt_, sizeof(ambient_light_input_rpt_t), |
| zx_clock_get_monotonic()); |
| } else if (input_rpt_.illuminance < threshold_low) { |
| input_rpt_.event = HID_USAGE_SENSOR_EVENT_LOW_THRESHOLD_CROSS_DOWNWARD_VAL; |
| client_.IoQueue(&input_rpt_, sizeof(ambient_light_input_rpt_t), |
| zx_clock_get_monotonic()); |
| } |
| } |
| // If report could not be filled, we do not ioqueue |
| irq_rearm_timeout = zx_deadline_after(INTERRUPTS_HYSTERESIS); |
| } |
| break; |
| case TCS_REARM_IRQ: |
| // rearm interrupt at the device level |
| { |
| fbl::AutoLock lock(&i2c_lock_); |
| uint8_t cmd[] = {TCS_I2C_AICLEAR, 0x00}; |
| status = i2c_.WriteSync(cmd, sizeof(cmd)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Tcs3400Device::Thread: i2c_write_sync failed: %d", status); |
| // Continue on error, future transactions may succeed |
| } |
| irq_rearm_timeout = ZX_TIME_INFINITE; |
| } |
| break; |
| case TCS_POLL: { |
| { |
| fbl::AutoLock lock(&client_input_lock_); |
| if (client_.is_valid()) { |
| FillInputRpt(); // We ioqueue even if report filling failed reporting bad state |
| input_rpt_.event = HID_USAGE_SENSOR_EVENT_PERIOD_EXCEEDED_VAL; |
| client_.IoQueue(&input_rpt_, sizeof(ambient_light_input_rpt_t), |
| zx_clock_get_monotonic()); |
| } |
| } |
| { |
| fbl::AutoLock lock(&feature_lock_); |
| poll_timeout += ZX_MSEC(feature_rpt_.interval_ms); |
| zx_time_t now = zx_clock_get_monotonic(); |
| if (now > poll_timeout) { |
| poll_timeout = zx_deadline_after(ZX_MSEC(feature_rpt_.interval_ms)); |
| } |
| } |
| break; |
| } |
| } |
| } |
| return thrd_success; |
| } |
| |
| zx_status_t Tcs3400Device::HidbusStart(const hidbus_ifc_protocol_t* ifc) { |
| fbl::AutoLock lock(&client_input_lock_); |
| if (client_.is_valid()) { |
| return ZX_ERR_ALREADY_BOUND; |
| } else { |
| client_ = ddk::HidbusIfcProtocolClient(ifc); |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t Tcs3400Device::HidbusQuery(uint32_t options, hid_info_t* info) { |
| if (!info) { |
| return ZX_ERR_INVALID_ARGS; |
| } |
| info->dev_num = 0; |
| info->device_class = HID_DEVICE_CLASS_OTHER; |
| info->boot_device = false; |
| info->vendor_id = static_cast<uint32_t>(llcpp::fuchsia::input::report::VendorId::GOOGLE); |
| info->product_id = |
| static_cast<uint32_t>(llcpp::fuchsia::input::report::VendorGoogleProductId::AMS_LIGHT_SENSOR); |
| |
| return ZX_OK; |
| } |
| |
| void Tcs3400Device::HidbusStop() {} |
| |
| zx_status_t Tcs3400Device::HidbusGetDescriptor(hid_description_type_t desc_type, |
| void* out_data_buffer, size_t data_size, |
| size_t* out_data_actual) { |
| const uint8_t* desc; |
| size_t desc_size = get_ambient_light_report_desc(&desc); |
| |
| if (data_size < desc_size) { |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| |
| memcpy(out_data_buffer, desc, desc_size); |
| *out_data_actual = desc_size; |
| return ZX_OK; |
| } |
| |
| zx_status_t Tcs3400Device::HidbusGetReport(uint8_t rpt_type, uint8_t rpt_id, void* data, size_t len, |
| size_t* out_len) { |
| if (rpt_id != AMBIENT_LIGHT_RPT_ID_INPUT && rpt_id != AMBIENT_LIGHT_RPT_ID_FEATURE) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| *out_len = (rpt_id == AMBIENT_LIGHT_RPT_ID_INPUT) ? sizeof(ambient_light_input_rpt_t) |
| : sizeof(ambient_light_feature_rpt_t); |
| if (*out_len > len) { |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| if (rpt_id == AMBIENT_LIGHT_RPT_ID_INPUT) { |
| fbl::AutoLock lock(&client_input_lock_); |
| FillInputRpt(); |
| auto out = static_cast<ambient_light_input_rpt_t*>(data); |
| *out = input_rpt_; // TA doesn't work on a memcpy taking an address as in &input_rpt_ |
| } else { |
| fbl::AutoLock lock(&feature_lock_); |
| auto out = static_cast<ambient_light_feature_rpt_t*>(data); |
| *out = feature_rpt_; // TA doesn't work on a memcpy taking an address as in &feature_rpt_ |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t Tcs3400Device::HidbusSetReport(uint8_t rpt_type, uint8_t rpt_id, const void* data, |
| size_t len) { |
| if (rpt_id != AMBIENT_LIGHT_RPT_ID_FEATURE) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| if (len < sizeof(ambient_light_feature_rpt_t)) { |
| return ZX_ERR_BUFFER_TOO_SMALL; |
| } |
| { |
| fbl::AutoLock lock(&feature_lock_); |
| auto* out = static_cast<const ambient_light_feature_rpt_t*>(data); |
| feature_rpt_ = *out; // TA doesn't work on a memcpy taking an address as in &feature_rpt_ |
| } |
| |
| zx_port_packet packet = {TCS_CONFIGURE, ZX_PKT_TYPE_USER, ZX_OK, {}}; |
| zx_status_t status = port_.queue(&packet); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "Tcs3400Device::HidbusSetReport: zx_port_queue failed: %d", status); |
| return ZX_ERR_INTERNAL; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t Tcs3400Device::HidbusGetIdle(uint8_t rpt_id, uint8_t* duration) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t Tcs3400Device::HidbusSetIdle(uint8_t rpt_id, uint8_t duration) { |
| return ZX_ERR_NOT_SUPPORTED; |
| } |
| |
| zx_status_t Tcs3400Device::HidbusGetProtocol(uint8_t* protocol) { return ZX_ERR_NOT_SUPPORTED; } |
| |
| zx_status_t Tcs3400Device::HidbusSetProtocol(uint8_t protocol) { return ZX_OK; } |
| |
| // static |
| zx_status_t Tcs3400Device::Create(void* ctx, zx_device_t* parent) { |
| ddk::CompositeProtocolClient composite(parent); |
| if (!composite.is_valid()) { |
| zxlogf(ERROR, "Could not get composite protocol"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| ddk::I2cChannel channel(composite, "i2c"); |
| if (!channel.is_valid()) { |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| ddk::GpioProtocolClient gpio(composite, "gpio"); |
| if (!gpio.is_valid()) { |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| zx::port port; |
| zx_status_t status = zx::port::create(ZX_PORT_BIND_TO_INTERRUPT, &port); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s port_create failed: %d", __FILE__, status); |
| return status; |
| } |
| |
| auto dev = |
| std::make_unique<tcs::Tcs3400Device>(parent, std::move(channel), gpio, std::move(port)); |
| status = dev->Bind(); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s bind failed: %d", __FILE__, status); |
| return status; |
| } |
| |
| status = dev->DdkAdd("tcs-3400"); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s DdkAdd failed: %d", __FILE__, status); |
| return status; |
| } |
| |
| // devmgr is now in charge of the memory for dev |
| __UNUSED auto ptr = dev.release(); |
| return status; |
| } |
| |
| zx_status_t Tcs3400Device::InitGain(uint8_t gain) { |
| if (!(gain == 1 || gain == 4 || gain == 16 || gain == 64)) { |
| zxlogf(WARNING, "%s Invalid gain (%u) using gain = 1", __FILE__, gain); |
| gain = 1; |
| } |
| |
| again_ = gain; |
| zxlogf(DEBUG, "again (%u)", again_); |
| |
| uint8_t reg; |
| // clang-format off |
| if (gain == 1) reg = 0; |
| if (gain == 4) reg = 1; |
| if (gain == 16) reg = 2; |
| if (gain == 64) reg = 3; |
| // clang-format on |
| |
| const uint8_t command[2] = {TCS_I2C_CONTROL, reg}; |
| fbl::AutoLock lock(&i2c_lock_); |
| auto status = i2c_.WriteSync(command, countof(command)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s Setting gain failed %d", __FILE__, status); |
| return status; |
| } |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Tcs3400Device::InitMetadata() { |
| metadata::LightSensorParams parameters = {}; |
| size_t actual = {}; |
| auto status = device_get_metadata(parent(), DEVICE_METADATA_PRIVATE, ¶meters, |
| sizeof(metadata::LightSensorParams), &actual); |
| if (status != ZX_OK || sizeof(metadata::LightSensorParams) != actual) { |
| zxlogf(ERROR, "%s Getting metadata failed %d", __FILE__, status); |
| return status; |
| } |
| |
| // ATIME = 256 - Integration Time / 2.4 ms. |
| if (parameters.integration_time_ms <= 615) { |
| atime_ = static_cast<uint8_t>(256 - (parameters.integration_time_ms * 10 / 24)); |
| } else { |
| atime_ = 1; |
| zxlogf(WARNING, "%s Invalid integration time (%u) using atime = %u", __FILE__, |
| parameters.integration_time_ms, atime_); |
| } |
| |
| zxlogf(DEBUG, "atime (%u)", atime_); |
| { |
| fbl::AutoLock lock(&i2c_lock_); |
| const uint8_t command[2] = {TCS_I2C_ATIME, atime_}; |
| status = i2c_.WriteSync(command, countof(command)); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s Setting integration time failed %d", __FILE__, status); |
| return status; |
| } |
| } |
| |
| status = InitGain(parameters.gain); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| // Set the default features and send a configuration packet. |
| { |
| fbl::AutoLock lock(&feature_lock_); |
| // The device will trigger an interrupt outside the thresholds. These default threshold |
| // values effectively disable interrupts since we can't be outside this range, interrupts |
| // get effectively enabled when we configure a range that could trigger. |
| feature_rpt_.threshold_low = 0x0000; |
| feature_rpt_.threshold_high = 0xFFFF; |
| feature_rpt_.interval_ms = parameters.polling_time_ms; |
| feature_rpt_.state = HID_USAGE_SENSOR_STATE_INITIALIZING_VAL; |
| } |
| zx_port_packet packet = {TCS_CONFIGURE, ZX_PKT_TYPE_USER, ZX_OK, {}}; |
| status = port_.queue(&packet); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s zx_port_queue failed: %d", __FILE__, status); |
| return status; |
| } |
| return ZX_OK; |
| } |
| |
| zx_status_t Tcs3400Device::Bind() { |
| { |
| fbl::AutoLock al(&i2c_lock_); |
| gpio_.ConfigIn(GPIO_NO_PULL); |
| |
| auto status = gpio_.GetInterrupt(ZX_INTERRUPT_MODE_EDGE_LOW, &irq_); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s gpio_get_interrupt failed: %d", __FILE__, status); |
| return status; |
| } |
| } |
| |
| zx_status_t status = irq_.bind(port_, TCS_INTERRUPT, 0); |
| if (status != ZX_OK) { |
| zxlogf(ERROR, "%s zx_interrupt_bind failed: %d", __FILE__, status); |
| return status; |
| } |
| |
| status = InitMetadata(); |
| if (status != ZX_OK) { |
| return status; |
| } |
| |
| int rc = thrd_create_with_name( |
| &thread_, [](void* arg) -> int { return reinterpret_cast<Tcs3400Device*>(arg)->Thread(); }, |
| reinterpret_cast<void*>(this), "tcs3400-thread"); |
| if (rc != thrd_success) { |
| return ZX_ERR_INTERNAL; |
| } |
| |
| return ZX_OK; |
| } |
| |
| void Tcs3400Device::ShutDown() { |
| zx_port_packet packet = {TCS_SHUTDOWN, ZX_PKT_TYPE_USER, ZX_OK, {}}; |
| zx_status_t status = port_.queue(&packet); |
| ZX_ASSERT(status == ZX_OK); |
| if (thread_) { |
| thrd_join(thread_, NULL); |
| } |
| irq_.destroy(); |
| { |
| fbl::AutoLock lock(&client_input_lock_); |
| client_.clear(); |
| } |
| } |
| |
| void Tcs3400Device::DdkUnbind(ddk::UnbindTxn txn) { |
| ShutDown(); |
| txn.Reply(); |
| } |
| |
| void Tcs3400Device::DdkRelease() { delete this; } |
| |
| static constexpr zx_driver_ops_t driver_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = Tcs3400Device::Create; |
| return ops; |
| }(); |
| |
| } // namespace tcs |
| |
| // clang-format off |
| ZIRCON_DRIVER_BEGIN(tcs3400_light, tcs::driver_ops, "zircon", "0.1", 4) |
| BI_ABORT_IF(NE, BIND_PROTOCOL, ZX_PROTOCOL_COMPOSITE), |
| BI_ABORT_IF(NE, BIND_PLATFORM_DEV_VID, PDEV_VID_AMS), |
| BI_ABORT_IF(NE, BIND_PLATFORM_DEV_PID, PDEV_PID_AMS_TCS3400), |
| BI_MATCH_IF(EQ, BIND_PLATFORM_DEV_DID, PDEV_DID_AMS_LIGHT), |
| ZIRCON_DRIVER_END(tcs3400_light) |
| // clang-format on |