| // 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 "ti-lp8556.h" |
| |
| #include <lib/ddk/debug.h> |
| #include <lib/ddk/metadata.h> |
| #include <lib/ddk/platform-defs.h> |
| #include <lib/device-protocol/i2c.h> |
| #include <lib/device-protocol/pdev.h> |
| #include <math.h> |
| |
| #include <algorithm> |
| |
| #include <ddktl/fidl.h> |
| #include <fbl/algorithm.h> |
| #include <fbl/alloc_checker.h> |
| |
| #include "src/ui/backlight/drivers/ti-lp8556/ti-lp8556-bind.h" |
| #include "ti-lp8556Metadata.h" |
| |
| namespace ti { |
| |
| // Refer to <internal>/vendor/amlogic/video-common/ambient_temp/lp8556.cc |
| // Lookup tables containing the slope and y-intercept for a linear equation used |
| // to fit the (power / |brightness_to_current_scalar_|) per vendor for |
| // brightness levels below |kMinTableBrightness|. The power can be calculated |
| // from these scalars by: |
| // (slope * brightness + intercept) * |brightness_to_current_scalar_|. |
| constexpr std::array<double, |
| static_cast<std::size_t>(Lp8556Device::PanelType::kNumTypes)> |
| kLowBrightnessSlopeTable = { |
| 22.4, // PanelType::kBoe |
| 22.2, // PanelType::kKd |
| }; |
| constexpr std::array<double, |
| static_cast<std::size_t>(Lp8556Device::PanelType::kNumTypes)> |
| kLowBrightnessInterceptTable = { |
| 1236.0, // PanelType::kBoe |
| 1319.0, // PanelType::kKd |
| }; |
| |
| // Lookup tables for backlight driver voltage as a function of the backlight |
| // brightness. The index for each sub-table corresponds to a PanelType, and |
| // allows for the backlight voltage to vary with panel vendor. Starting from a |
| // brightness level of |kMinTableBrightness|, each index of each sub-table |
| // corresponds to a jump of |kBrightnessStep| in brightness up to the maximum |
| // value of |kMaxBrightnessSetting|. |
| constexpr std::array<std::array<double, kTableSize>, |
| static_cast<std::size_t>(Lp8556Device::PanelType::kNumTypes)> |
| kVoltageTable = {{ |
| // PanelType::kBoe |
| {19.80, 19.80, 19.80, 19.80, 19.90, 20.00, 20.10, 20.20, 20.30, 20.40, 20.50, 20.53, 20.53, |
| 20.53, 20.53, 20.53}, |
| // PanelType::kKd |
| {19.67, 19.67, 19.67, 19.67, 19.77, 19.93, 20.03, 20.13, 20.20, 20.27, 20.37, 20.37, 20.37, |
| 20.37, 20.37, 20.37}, |
| }}; |
| |
| // Lookup table for backlight driver efficiency as a function of the backlight |
| // brightness. Starting from a brightness level of |kMinTableBrightness|, each |
| // index of the table corresponds to a jump of |kBrightnessStep| in brightness |
| // up to the maximum value of |kMaxBrightnessSetting|. |
| constexpr std::array<double, kTableSize> kEfficiencyTable = { |
| 0.6680, 0.7784, 0.8240, 0.8484, 0.8634, 0.8723, 0.8807, 0.8860, |
| 0.8889, 0.8915, 0.8953, 0.8983, 0.9003, 0.9034, 0.9049, 0.9060}; |
| |
| // The max current value in the table is determined by the value of the three |
| // max current bits within the Lp8556 CFG1 register. The value of these bits can |
| // be obtained from the max_current sysfs node exposed by the driver. The |
| // current values in the table are expressed in mA. |
| constexpr std::array<double, 8> kMaxCurrentTable = {5.0, 10.0, 15.0, 20.0, 23.0, 25.0, 30.0, 50.0}; |
| |
| void Lp8556Device::DdkUnbind(ddk::UnbindTxn txn) { txn.Reply(); } |
| |
| void Lp8556Device::DdkRelease() { delete this; } |
| |
| zx_status_t Lp8556Device::GetBacklightState(bool* power, double* brightness) { |
| *power = power_; |
| *brightness = brightness_; |
| return ZX_OK; |
| } |
| |
| zx_status_t Lp8556Device::SetBacklightState(bool power, double brightness) { |
| brightness = std::max(brightness, 0.0); |
| brightness = std::min(brightness, 1.0); |
| uint16_t brightness_reg_value = static_cast<uint16_t>(ceil(brightness * kBrightnessRegMaxValue)); |
| if (brightness != brightness_) { |
| // LSB should be updated before MSB. Writing to MSB triggers the brightness change. |
| uint8_t buf[2]; |
| buf[0] = kBacklightBrightnessLsbReg; |
| buf[1] = static_cast<uint8_t>(brightness_reg_value & kBrightnessLsbMask); |
| zx_status_t status = i2c_.WriteSync(buf, sizeof(buf)); |
| if (status != ZX_OK) { |
| LOG_ERROR("Failed to set brightness LSB register\n"); |
| return status; |
| } |
| |
| uint8_t msb_reg_value; |
| status = i2c_.ReadSync(kBacklightBrightnessMsbReg, &msb_reg_value, 1); |
| if (status != ZX_OK) { |
| LOG_ERROR("Failed to get brightness MSB register\n"); |
| return status; |
| } |
| |
| // The low 4-bits contain the brightness MSB. Keep the remaining bits unchanged. |
| msb_reg_value &= static_cast<uint8_t>(~kBrightnessMsbByteMask); |
| msb_reg_value |= |
| (static_cast<uint8_t>((brightness_reg_value & kBrightnessMsbMask) >> kBrightnessMsbShift)); |
| |
| buf[0] = kBacklightBrightnessMsbReg; |
| buf[1] = msb_reg_value; |
| status = i2c_.WriteSync(buf, sizeof(buf)); |
| if (status != ZX_OK) { |
| LOG_ERROR("Failed to set brightness MSB register\n"); |
| return status; |
| } |
| |
| auto persistent_brightness = BrightnessStickyReg::Get().ReadFrom(&mmio_); |
| persistent_brightness.set_brightness(brightness_reg_value & kBrightnessRegMask); |
| persistent_brightness.set_is_valid(1); |
| persistent_brightness.WriteTo(&mmio_); |
| } |
| |
| if (power != power_) { |
| uint8_t buf[2]; |
| buf[0] = kDeviceControlReg; |
| buf[1] = power ? kBacklightOn : kBacklightOff; |
| zx_status_t status = i2c_.WriteSync(buf, sizeof(buf)); |
| if (status != ZX_OK) { |
| LOG_ERROR("Failed to set device control register\n"); |
| return status; |
| } |
| |
| if (power) { |
| for (size_t i = 0; i < metadata_.register_count; i += 2) { |
| if ((status = i2c_.WriteSync(&metadata_.registers[i], 2)) != ZX_OK) { |
| LOG_ERROR("Failed to set register 0x%02x: %d\n", metadata_.registers[i], status); |
| return status; |
| } |
| } |
| |
| buf[0] = kCfg2Reg; |
| buf[1] = cfg2_; |
| status = i2c_.WriteSync(buf, sizeof(buf)); |
| if (status != ZX_OK) { |
| LOG_ERROR("Failed to set cfg2 register\n"); |
| return status; |
| } |
| } |
| } |
| |
| // update internal values |
| power_ = power; |
| brightness_ = brightness; |
| power_property_.Set(power_); |
| brightness_property_.Set(brightness_); |
| backlight_power_ = GetBacklightPower(brightness_reg_value); |
| return ZX_OK; |
| } |
| |
| void Lp8556Device::GetStateNormalized(GetStateNormalizedCompleter::Sync& completer) { |
| FidlBacklight::wire::State state = {}; |
| auto status = GetBacklightState(&state.backlight_on, &state.brightness); |
| if (status == ZX_OK) { |
| completer.ReplySuccess(state); |
| } else { |
| completer.ReplyError(status); |
| } |
| } |
| |
| void Lp8556Device::SetStateNormalized(FidlBacklight::wire::State state, |
| SetStateNormalizedCompleter::Sync& completer) { |
| auto status = SetBacklightState(state.backlight_on, state.brightness); |
| if (status == ZX_OK) { |
| completer.ReplySuccess(); |
| } else { |
| completer.ReplyError(status); |
| } |
| } |
| |
| void Lp8556Device::GetStateAbsolute(GetStateAbsoluteCompleter::Sync& completer) { |
| if (!max_absolute_brightness_nits_.has_value()) { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| return; |
| } |
| if (scale_ != calibrated_scale_) { |
| LOG_ERROR("Can't get absolute state with non-calibrated current scale\n"); |
| completer.ReplyError(ZX_ERR_BAD_STATE); |
| return; |
| } |
| |
| FidlBacklight::wire::State state = {}; |
| auto status = GetBacklightState(&state.backlight_on, &state.brightness); |
| if (status == ZX_OK) { |
| state.brightness *= max_absolute_brightness_nits_.value(); |
| completer.ReplySuccess(state); |
| } else { |
| completer.ReplyError(status); |
| } |
| } |
| |
| void Lp8556Device::SetStateAbsolute(FidlBacklight::wire::State state, |
| SetStateAbsoluteCompleter::Sync& completer) { |
| if (!max_absolute_brightness_nits_.has_value()) { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| return; |
| } |
| |
| // Restore the calibrated current scale that the bootloader set. This and the maximum brightness |
| // are the only values we have that can be used to set the absolute brightness in nits. |
| auto status = SetCurrentScale(calibrated_scale_); |
| if (status != ZX_OK) { |
| completer.ReplyError(status); |
| return; |
| } |
| |
| status = SetBacklightState(state.backlight_on, |
| state.brightness / max_absolute_brightness_nits_.value()); |
| if (status == ZX_OK) { |
| completer.ReplySuccess(); |
| } else { |
| completer.ReplyError(status); |
| } |
| } |
| |
| void Lp8556Device::GetMaxAbsoluteBrightness(GetMaxAbsoluteBrightnessCompleter::Sync& completer) { |
| if (max_absolute_brightness_nits_.has_value()) { |
| completer.ReplySuccess(max_absolute_brightness_nits_.value()); |
| } else { |
| completer.ReplyError(ZX_ERR_NOT_SUPPORTED); |
| } |
| } |
| |
| void Lp8556Device::SetNormalizedBrightnessScale( |
| double scale, SetNormalizedBrightnessScaleCompleter::Sync& completer) { |
| scale = std::clamp(scale, 0.0, 1.0); |
| |
| zx_status_t status = SetCurrentScale(static_cast<uint16_t>(scale * kBrightnessRegMaxValue)); |
| if (status != ZX_OK) { |
| completer.ReplyError(status); |
| } else { |
| completer.ReplySuccess(); |
| } |
| } |
| |
| void Lp8556Device::GetNormalizedBrightnessScale( |
| GetNormalizedBrightnessScaleCompleter::Sync& completer) { |
| completer.ReplySuccess(static_cast<double>(scale_) / kBrightnessRegMaxValue); |
| } |
| |
| void Lp8556Device::GetPowerWatts(GetPowerWattsCompleter::Sync& completer) { |
| completer.ReplySuccess(backlight_power_); |
| } |
| |
| zx_status_t Lp8556Device::DdkMessage(fidl_incoming_msg_t* msg, fidl_txn_t* txn) { |
| DdkTransaction transaction(txn); |
| if (fidl::WireTryDispatch<FidlBacklight::Device>(this, msg, &transaction) == |
| ::fidl::DispatchResult::kFound) { |
| return transaction.Status(); |
| } |
| fidl::WireDispatch<FidlPowerSensor::Device>(this, msg, &transaction); |
| return transaction.Status(); |
| } |
| |
| zx_status_t Lp8556Device::Init() { |
| root_ = inspector_.GetRoot().CreateChild("ti-lp8556"); |
| double brightness_nits = 0.0; |
| size_t actual; |
| zx_status_t status = device_get_metadata(parent(), DEVICE_METADATA_BACKLIGHT_MAX_BRIGHTNESS_NITS, |
| &brightness_nits, sizeof(brightness_nits), &actual); |
| if (status == ZX_OK && actual == sizeof(brightness_nits)) { |
| SetMaxAbsoluteBrightnessNits(brightness_nits); |
| } |
| status = device_get_metadata(parent(), DEVICE_METADATA_PRIVATE, &metadata_, sizeof(metadata_), |
| &actual); |
| // Supplying this metadata is optional. |
| if (status == ZX_OK) { |
| if (metadata_.register_count % (2 * sizeof(uint8_t)) != 0) { |
| LOG_ERROR("Register metadata is invalid\n"); |
| return ZX_ERR_INVALID_ARGS; |
| } else if (actual != sizeof(metadata_)) { |
| LOG_ERROR("Too many registers specified in metadata\n"); |
| return ZX_ERR_OUT_OF_RANGE; |
| } |
| |
| for (size_t i = 0; i < metadata_.register_count; i += 2) { |
| if ((status = i2c_.WriteSync(&metadata_.registers[i], 2)) != ZX_OK) { |
| LOG_ERROR("Failed to set register 0x%02x: %d\n", metadata_.registers[i], status); |
| return status; |
| } |
| } |
| } |
| |
| auto persistent_brightness = BrightnessStickyReg::Get().ReadFrom(&mmio_); |
| |
| if (persistent_brightness.is_valid()) { |
| double brightness = |
| static_cast<double>(persistent_brightness.brightness()) / kBrightnessRegMaxValue; |
| |
| if ((status = SetBacklightState(brightness > 0, brightness)) != ZX_OK) { |
| LOG_ERROR("Could not set persistent brightness value: %f\n", brightness); |
| } else { |
| LOG_INFO("Successfully set persistent brightness value: %f\n", brightness); |
| } |
| persistent_brightness_property_ = |
| root_.CreateUint("persistent_brightness", persistent_brightness.brightness()); |
| } |
| |
| if ((i2c_.ReadSync(kCfg2Reg, &cfg2_, 1) != ZX_OK) || (cfg2_ == 0)) { |
| cfg2_ = kCfg2Default; |
| } |
| |
| uint8_t buf[2]; |
| if ((i2c_.ReadSync(kCurrentLsbReg, buf, sizeof(buf))) != ZX_OK) { |
| LOG_ERROR("Could not read current scale value: %d\n", status); |
| return status; |
| } |
| scale_ = static_cast<uint16_t>(buf[0] | (buf[1] << kBrightnessMsbShift)) & kBrightnessRegMask; |
| calibrated_scale_ = scale_; |
| |
| brightness_property_ = root_.CreateDouble("brightness", brightness_); |
| scale_property_ = root_.CreateUint("scale", scale_); |
| calibrated_scale_property_ = root_.CreateUint("calibrated_scale", calibrated_scale_); |
| power_property_ = root_.CreateBool("power", power_); |
| // max_absolute_brightness_nits will be initialized in SetMaxAbsoluteBrightnessNits. |
| uint8_t max_current_idx; |
| i2c_.ReadSync(kCfgReg, &max_current_idx, sizeof(max_current_idx)); |
| max_current_idx = (max_current_idx >> 4) & 0b111; |
| max_current_ = kMaxCurrentTable[max_current_idx]; |
| |
| return ZX_OK; |
| } |
| |
| zx_status_t Lp8556Device::SetCurrentScale(uint16_t scale) { |
| scale &= kBrightnessRegMask; |
| |
| if (scale == scale_) { |
| return ZX_OK; |
| } |
| |
| uint8_t msb_reg_value; |
| zx_status_t status = i2c_.ReadSync(kCfgReg, &msb_reg_value, sizeof(msb_reg_value)); |
| if (status != ZX_OK) { |
| LOG_ERROR("Failed to get current scale register: %d", status); |
| return status; |
| } |
| msb_reg_value &= ~kBrightnessMsbByteMask; |
| |
| const uint8_t buf[] = { |
| kCurrentLsbReg, |
| static_cast<uint8_t>(scale & kBrightnessLsbMask), |
| static_cast<uint8_t>(msb_reg_value | (scale >> kBrightnessMsbShift)), |
| }; |
| if ((status = i2c_.WriteSync(buf, sizeof(buf))) != ZX_OK) { |
| LOG_ERROR("Failed to set current scale register: %d", status); |
| return status; |
| } |
| |
| scale_ = scale; |
| scale_property_.Set(scale); |
| return ZX_OK; |
| } |
| |
| double Lp8556Device::GetBacklightPower(double backlight_brightness) { |
| // For brightness values less than |kMinTableBrightness|, estimate the power |
| // on a per-vendor basis from a linear equation derived from validation data. |
| if (backlight_brightness < kMinTableBrightness) { |
| std::size_t panel_type_index = static_cast<std::size_t>(GetPanelType()); |
| double slope = kLowBrightnessSlopeTable[panel_type_index]; |
| double intercept = kLowBrightnessInterceptTable[panel_type_index]; |
| return (slope * backlight_brightness + intercept) * GetBrightnesstoCurrentScalar(); |
| } |
| |
| // For brightness values in the range [|kMinTableBrightness|, |
| // |kMaxBrightnessSetting|], use the voltage and efficiency lookup tables |
| // derived from validation data to estimate the power. |
| double backlight_voltage = GetBacklightVoltage(backlight_brightness, GetPanelType()); |
| double current_amp = GetBrightnesstoCurrentScalar() * backlight_brightness; |
| double driver_efficiency = GetDriverEfficiency(backlight_brightness); |
| return backlight_voltage * current_amp / driver_efficiency; |
| } |
| |
| double Lp8556Device::GetBrightnesstoCurrentScalar() { |
| double setpoint_current_setting = scale_ / kMaxCurrentSetting; |
| double max_current_amp = max_current_ / kMilliampPerAmp; |
| // The setpoint current refers to the backlight current for a single driver |
| // channel, assuming that the backlight brightness setting is at its max value |
| // of 4095 (100%). |
| double setpoint_current_amp = (setpoint_current_setting / kMaxCurrentSetting) * max_current_amp; |
| // The scalar returned is equal to: |
| // 6 Driver Channels * Setpoint Current per Channel / Max Brightness Setting |
| // When this value is multiplied by the backlight brightness setting, it |
| // yields the backlight current in Amps. |
| return kNumBacklightDriverChannels * setpoint_current_amp / kMaxBrightnessSetting; |
| } |
| |
| double Lp8556Device::GetBacklightVoltage(double backlight_brightness, PanelType panel_type) { |
| std::size_t panel_type_index = static_cast<std::size_t>(panel_type); |
| |
| // Backlight is at max brightness |
| if (backlight_brightness == kMaxBrightnessSetting) { |
| return kVoltageTable[panel_type_index].back(); |
| } |
| |
| // Backlight is at |kMinTableBrightness| |
| if (backlight_brightness == kMinTableBrightness) { |
| return kVoltageTable[panel_type_index].front(); |
| } |
| |
| double integral; |
| double fractional = modf(backlight_brightness / kBrightnessStep, &integral); |
| std::size_t table_index = static_cast<std::size_t>(integral) - 1; |
| if (table_index + 1 >= kVoltageTable[panel_type_index].size()) { |
| LOG_ERROR("Invalid backlight brightness: %f", backlight_brightness); |
| return kVoltageTable[panel_type_index].back(); |
| } |
| double lower_voltage = kVoltageTable[panel_type_index][table_index]; |
| double upper_voltage = kVoltageTable[panel_type_index][table_index + 1]; |
| return (upper_voltage - lower_voltage) * fractional + lower_voltage; |
| } |
| |
| double Lp8556Device::GetDriverEfficiency(double backlight_brightness) { |
| // Backlight is at max brightness |
| if (backlight_brightness == kMaxBrightnessSetting) { |
| return kEfficiencyTable.back(); |
| } |
| // Backlight is at |kMinTableBrightness| |
| if (backlight_brightness == kMinTableBrightness) { |
| return kEfficiencyTable.front(); |
| } |
| double integral; |
| double fractional = modf(backlight_brightness / kBrightnessStep, &integral); |
| std::size_t table_index = static_cast<std::size_t>(integral) - 1; |
| if (table_index + 1 >= kEfficiencyTable.size()) { |
| LOG_ERROR("Invalid backlight brightness: %f", backlight_brightness); |
| return kEfficiencyTable.back(); |
| } |
| double lower_efficiency = kEfficiencyTable[table_index]; |
| double upper_efficiency = kEfficiencyTable[table_index + 1]; |
| return (upper_efficiency - lower_efficiency) * fractional + lower_efficiency; |
| } |
| |
| Lp8556Device::PanelType Lp8556Device::GetPanelType() { |
| if (metadata_.panel_id == 0 || metadata_.panel_id == 1) { |
| return Lp8556Device::PanelType::kKd; |
| } |
| return Lp8556Device::PanelType::kBoe; |
| } |
| |
| zx_status_t ti_lp8556_bind(void* ctx, zx_device_t* parent) { |
| // Get platform device protocol |
| auto pdev = ddk::PDev::FromFragment(parent); |
| if (!pdev.is_valid()) { |
| LOG_ERROR("Could not get PDEV protocol\n"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| // Map MMIO |
| std::optional<ddk::MmioBuffer> mmio; |
| zx_status_t status = pdev.MapMmio(0, &mmio); |
| if (status != ZX_OK) { |
| LOG_ERROR("Could not map mmio %d\n", status); |
| return status; |
| } |
| |
| // Obtain I2C protocol needed to control backlight |
| ddk::I2cChannel i2c(parent, "i2c"); |
| if (!i2c.is_valid()) { |
| LOG_ERROR("Could not obtain I2C protocol\n"); |
| return ZX_ERR_NO_RESOURCES; |
| } |
| |
| fbl::AllocChecker ac; |
| auto dev = |
| fbl::make_unique_checked<ti::Lp8556Device>(&ac, parent, std::move(i2c), *std::move(mmio)); |
| if (!ac.check()) { |
| return ZX_ERR_NO_MEMORY; |
| } |
| |
| if ((status = dev->Init()) != ZX_OK) { |
| return status; |
| } |
| |
| status = dev->DdkAdd(ddk::DeviceAddArgs("ti-lp8556").set_inspect_vmo(dev->InspectVmo())); |
| if (status != ZX_OK) { |
| LOG_ERROR("Could not add device\n"); |
| return status; |
| } |
| |
| // devmgr is now in charge of memory for dev |
| __UNUSED auto ptr = dev.release(); |
| |
| return status; |
| } |
| |
| static constexpr zx_driver_ops_t ti_lp8556_driver_ops = []() { |
| zx_driver_ops_t ops = {}; |
| ops.version = DRIVER_OPS_VERSION; |
| ops.bind = ti_lp8556_bind; |
| return ops; |
| }(); |
| |
| } // namespace ti |
| |
| ZIRCON_DRIVER(ti_lp8556, ti::ti_lp8556_driver_ops, "TI-LP8556", "0.1"); |