blob: 59be953214dd296263656508e77c582790e01c24 [file] [log] [blame]
// Copyright 2020 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-ina231.h"
#include <endian.h>
#include <lib/ddk/binding_driver.h>
#include <lib/ddk/debug.h>
#include <lib/ddk/metadata.h>
#include <ddktl/fidl.h>
#include <fbl/auto_lock.h>
namespace {
// Choose 2048 for the calibration value so that the current and shunt voltage registers are the
// same. This results in a power resolution of 6.25 mW with a shunt resistance of 10 milli-ohms.
constexpr uint16_t kCalibrationValue = 2048;
// From the datasheet:
// Current resolution in A/bit = 0.00512 / (calibration value * shunt resistance in ohms)
// Power resolution in W/bit = current resolution in A/bit * 25
//
// We use shunt resistance in micro-ohms, so this becomes:
// Current resolution in A/bit = 5120.0 / (calibration value * shunt resistance in micro-ohms)
// Multiply by kFixedPointFactor to avoid truncation. To get the power in watts, multiply
// kPowerResolution by the power register value, divide by the shunt resistance in micro-ohms, then
// divide again by kFixedPointFactor.
constexpr uint64_t kFixedPointFactor = 1'000;
constexpr uint64_t kPowerResolution = (25ULL * 5'120 * kFixedPointFactor) / kCalibrationValue;
static_assert((kPowerResolution * kCalibrationValue) == (25ULL * 5'120 * kFixedPointFactor));
// Divide the bus voltage limit by this to get the alert limit register value.
constexpr uint64_t kMicrovoltsPerBit = 1'250;
constexpr float kMicrovoltsToVolts = 1000.0f * 1000.0f;
constexpr float kVoltsPerBit = kMicrovoltsToVolts / kMicrovoltsPerBit;
} // namespace
namespace power_sensor {
enum class Ina231Device::Register : uint8_t {
kConfigurationReg = 0,
kBusVoltageReg = 2,
kPowerReg = 3,
kCalibrationReg = 5,
kMaskEnableReg = 6,
kAlertLimitReg = 7,
};
zx_status_t Ina231Device::Create(void* ctx, zx_device_t* parent) {
ddk::I2cChannel i2c(parent, "i2c");
if (!i2c.is_valid()) {
zxlogf(ERROR, "Failed to get I2C protocol");
return ZX_ERR_NO_RESOURCES;
}
std::string name;
if (auto result = i2c.GetName(); result.ok()) {
name = std::string(result.value()->name.data(), result.value()->name.size());
}
Ina231Metadata metadata = {};
size_t actual = 0;
zx_status_t status = device_get_fragment_metadata(parent, "pdev", DEVICE_METADATA_PRIVATE,
&metadata, sizeof(metadata), &actual);
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to get metadata: %d", status);
return status;
}
if (actual != sizeof(metadata)) {
zxlogf(ERROR, "Expected %zu bytes of metadata, got %zu", sizeof(metadata), actual);
return ZX_ERR_NO_RESOURCES;
}
if (metadata.shunt_resistance_microohm == 0) {
zxlogf(ERROR, "Shunt resistance cannot be zero");
return ZX_ERR_INVALID_ARGS;
}
auto dev = std::make_unique<Ina231Device>(parent, metadata.shunt_resistance_microohm,
std::move(i2c), std::move(name));
if ((status = dev->Init(metadata)) != ZX_OK) {
return status;
}
auto endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
if (endpoints.is_error()) {
return endpoints.status_value();
}
std::array offers = {
fuchsia_hardware_power_sensor::Service::Name,
};
zx_device_prop_t props[] = {
{BIND_POWER_SENSOR_DOMAIN, 0, metadata.power_sensor_domain},
};
dev->outgoing_server_end_ = std::move(endpoints->server);
status = dev->DdkAdd(ddk::DeviceAddArgs("ti-ina231")
.set_props(props)
.set_fidl_service_offers(offers)
.set_outgoing_dir(endpoints->client.TakeChannel())
.set_proto_id(ZX_PROTOCOL_POWER_SENSOR)
.forward_metadata(parent, DEVICE_METADATA_PRIVATE));
if (status != ZX_OK) {
zxlogf(ERROR, "DdkAdd failed: %d", status);
return status;
}
[[maybe_unused]] auto* _ = dev.release();
return ZX_OK;
}
zx_status_t Ina231Device::PowerSensorConnectServer(zx::channel server) {
fidl::BindServer(loop_.dispatcher(),
fidl::ServerEnd<fuchsia_hardware_power_sensor::Device>(std::move(server)), this);
return ZX_OK;
}
void Ina231Device::GetPowerWatts(GetPowerWattsCompleter::Sync& completer) {
zx::result<uint16_t> power_reg;
{
fbl::AutoLock lock(&i2c_lock_);
power_reg = Read16(Register::kPowerReg);
}
if (power_reg.is_error()) {
completer.Close(power_reg.error_value());
return;
}
const uint64_t power = (power_reg.value() * kPowerResolution) / shunt_resistor_uohms_;
completer.ReplySuccess(static_cast<float>(power) / kFixedPointFactor);
}
void Ina231Device::GetVoltageVolts(GetVoltageVoltsCompleter::Sync& completer) {
zx::result<uint16_t> voltage_reg;
{
fbl::AutoLock lock(&i2c_lock_);
voltage_reg = Read16(Register::kBusVoltageReg);
}
if (voltage_reg.is_error()) {
completer.Close(voltage_reg.error_value());
return;
}
completer.ReplySuccess(static_cast<float>(voltage_reg.value()) / kVoltsPerBit);
}
void Ina231Device::GetSensorName(GetSensorNameCompleter::Sync& completer) {
completer.Reply(fidl::StringView::FromExternal(name_));
}
zx_status_t Ina231Device::Init(const Ina231Metadata& metadata) {
{
zx_status_t status = loop_.StartThread("TI INA231 loop thread");
if (status != ZX_OK) {
zxlogf(ERROR, "Failed to start thread: %d", status);
return status;
}
}
// Keep only the bits that are not defined in the datasheet, and clear the reset bit.
constexpr uint16_t kConfigurationRegMask = 0x7000;
fbl::AutoLock lock(&i2c_lock_);
auto status = Write16(Register::kCalibrationReg, kCalibrationValue);
if (status.is_error()) {
return status.error_value();
}
if (metadata.alert == Ina231Metadata::kAlertBusUnderVoltage) {
const uint64_t alert_limit_reg_value = metadata.bus_voltage_limit_microvolt / kMicrovoltsPerBit;
if (alert_limit_reg_value > UINT16_MAX) {
zxlogf(ERROR, "Bus voltage limit is out of range");
return ZX_ERR_OUT_OF_RANGE;
}
if ((status = Write16(Register::kAlertLimitReg, alert_limit_reg_value)).is_error()) {
return status.error_value();
}
}
if ((status = Write16(Register::kMaskEnableReg, metadata.alert)).is_error()) {
return status.error_value();
}
const zx::result<uint16_t> config_status = Read16(Register::kConfigurationReg);
if (config_status.is_error()) {
return config_status.error_value();
}
const uint16_t metadata_value = metadata.mode | (metadata.shunt_voltage_conversion_time << 3) |
(metadata.bus_voltage_conversion_time << 6) |
(metadata.averages << 9);
const uint16_t configuration_reg_value =
(config_status.value() & kConfigurationRegMask) | metadata_value;
if ((status = Write16(Register::kConfigurationReg, configuration_reg_value)).is_error()) {
return status.error_value();
}
return ZX_OK;
}
zx::result<uint16_t> Ina231Device::Read16(Register reg) {
const uint8_t address = static_cast<uint8_t>(reg);
uint16_t value = 0;
zx_status_t status = i2c_.WriteReadSync(&address, sizeof(address),
reinterpret_cast<uint8_t*>(&value), sizeof(value));
if (status != ZX_OK) {
zxlogf(ERROR, "I2C read failed: %d", status);
return zx::error(status);
}
return zx::ok(betoh16(value));
}
zx::result<> Ina231Device::Write16(Register reg, uint16_t value) {
uint8_t buffer[] = {static_cast<uint8_t>(reg), static_cast<uint8_t>(value >> 8),
static_cast<uint8_t>(value & 0xff)};
zx_status_t status = i2c_.WriteSync(buffer, sizeof(buffer));
if (status != ZX_OK) {
zxlogf(ERROR, "I2C write failed: %d", status);
return zx::error(status);
}
return zx::ok();
}
static constexpr zx_driver_ops_t ti_ina231_driver_ops = []() {
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = Ina231Device::Create;
return ops;
}();
} // namespace power_sensor
ZIRCON_DRIVER(ti_ina231, power_sensor::ti_ina231_driver_ops, "ti-ina231", "0.1");