Merge branch 'feature/rlcd-upstream-main' into feature/mcp-rlcd-custom
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
@@ -1,5 +0,0 @@
|
||||
# AW9523
|
||||
|
||||
Multi-functional GPIO expander and LED driver with I2C interface.
|
||||
|
||||
[Datasheet](https://www.alldatasheet.com/datasheet-pdf/download/1148542/AWINIC/AW9523B.html)
|
||||
@@ -1,33 +0,0 @@
|
||||
#include "Aw9523.h"
|
||||
|
||||
#define AW9523_REGISTER_P0 0x02
|
||||
#define AW9523_REGISTER_P1 0x03
|
||||
#define AW9523_REGISTER_CTL 0x11
|
||||
|
||||
bool Aw9523::readP0(uint8_t& output) const {
|
||||
return readRegister8(AW9523_REGISTER_P0, output);
|
||||
}
|
||||
|
||||
bool Aw9523::readP1(uint8_t& output) const {
|
||||
return readRegister8(AW9523_REGISTER_P1, output);
|
||||
}
|
||||
|
||||
bool Aw9523::readCTL(uint8_t& output) const {
|
||||
return readRegister8(AW9523_REGISTER_CTL, output);
|
||||
}
|
||||
|
||||
bool Aw9523::writeP0(uint8_t value) const {
|
||||
return writeRegister8(AW9523_REGISTER_P0, value);
|
||||
}
|
||||
|
||||
bool Aw9523::writeP1(uint8_t value) const {
|
||||
return writeRegister8(AW9523_REGISTER_P1, value);
|
||||
}
|
||||
|
||||
bool Aw9523::writeCTL(uint8_t value) const {
|
||||
return writeRegister8(AW9523_REGISTER_CTL, value);
|
||||
}
|
||||
|
||||
bool Aw9523::bitOnP1(uint8_t bitmask) const {
|
||||
return bitOn(AW9523_REGISTER_P1, bitmask);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/i2c/I2cDevice.h>
|
||||
|
||||
#define AW9523_ADDRESS 0x58
|
||||
|
||||
class Aw9523 : public tt::hal::i2c::I2cDevice {
|
||||
|
||||
public:
|
||||
|
||||
explicit Aw9523(::Device* controller) : I2cDevice(controller, AW9523_ADDRESS) {}
|
||||
|
||||
std::string getName() const final { return "AW9523"; }
|
||||
std::string getDescription() const final { return "GPIO expander with LED driver and I2C interface."; }
|
||||
|
||||
bool readP0(uint8_t& output) const;
|
||||
bool readP1(uint8_t& output) const;
|
||||
bool readCTL(uint8_t& output) const;
|
||||
|
||||
bool writeP0(uint8_t value) const;
|
||||
bool writeP1(uint8_t value) const;
|
||||
bool writeCTL(uint8_t value) const;
|
||||
|
||||
bool bitOnP1(uint8_t bitmask) const;
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS "Include"
|
||||
PRIV_INCLUDE_DIRS "Private"
|
||||
REQUIRES Tactility
|
||||
)
|
||||
@@ -1,59 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <axp192/axp192.h>
|
||||
#include <tactility/device.h>
|
||||
#include <Tactility/hal/power/PowerDevice.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class Axp192 final : public tt::hal::power::PowerDevice {
|
||||
|
||||
static int32_t i2cRead(void* handle, uint8_t address, uint8_t reg, uint8_t* buffer, uint16_t size);
|
||||
static int32_t i2cWrite(void* handle, uint8_t address, uint8_t reg, const uint8_t* buffer, uint16_t size);
|
||||
|
||||
public:
|
||||
|
||||
struct Configuration {
|
||||
::Device* controller;
|
||||
TickType_t readTimeout = 50 / portTICK_PERIOD_MS;
|
||||
TickType_t writeTimeout = 50 / portTICK_PERIOD_MS;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
std::unique_ptr<Configuration> configuration;
|
||||
|
||||
axp192_t axpDevice = {
|
||||
.read = i2cRead,
|
||||
.write = i2cWrite,
|
||||
.handle = this
|
||||
};
|
||||
|
||||
bool isInitialized = false;
|
||||
|
||||
public:
|
||||
|
||||
explicit Axp192(std::unique_ptr<Configuration> configuration) : configuration(std::move(configuration)) {}
|
||||
~Axp192() override {}
|
||||
|
||||
/**
|
||||
* @warning Must call this function before device can operate!
|
||||
* @param onInit
|
||||
*/
|
||||
bool init(const std::function<bool(axp192_t*)>& onInit) {
|
||||
isInitialized = onInit(&axpDevice);
|
||||
return isInitialized;
|
||||
}
|
||||
|
||||
axp192_t* getAxp192() { return &axpDevice; }
|
||||
|
||||
std::string getName() const override { return "AXP192"; }
|
||||
std::string getDescription() const override { return "AXP192 power management via I2C"; }
|
||||
|
||||
bool supportsMetric(MetricType type) const override;
|
||||
bool getMetric(MetricType type, MetricData& data) override;
|
||||
|
||||
bool supportsChargeControl() const override { return true; }
|
||||
bool isAllowedToCharge() const override;
|
||||
void setAllowedToCharge(bool canCharge) override;
|
||||
};
|
||||
@@ -1,213 +0,0 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2021 Mika Tuupola
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-cut-
|
||||
|
||||
This file is part of hardware agnostic I2C driver for AXP192:
|
||||
https://github.com/tuupola/axp192
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
Version: 0.6.0
|
||||
|
||||
*/
|
||||
|
||||
#ifndef _AXP192_H
|
||||
#define _AXP192_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define AXP192_ADDRESS (0x34)
|
||||
|
||||
/* Power control registers */
|
||||
#define AXP192_POWER_STATUS (0x00)
|
||||
#define AXP192_CHARGE_STATUS (0x01)
|
||||
#define AXP192_OTG_VBUS_STATUS (0x04)
|
||||
#define AXP192_DATA_BUFFER0 (0x06)
|
||||
#define AXP192_DATA_BUFFER1 (0x07)
|
||||
#define AXP192_DATA_BUFFER2 (0x08)
|
||||
#define AXP192_DATA_BUFFER3 (0x09)
|
||||
#define AXP192_DATA_BUFFER4 (0x0a)
|
||||
#define AXP192_DATA_BUFFER5 (0x0b)
|
||||
/* Output control: 2 EXTEN, 0 DCDC2 */
|
||||
#define AXP192_EXTEN_DCDC2_CONTROL (0x10)
|
||||
/* Power output control: 6 EXTEN, 4 DCDC2, 3 LDO3, 2 LDO2, 1 DCDC3, 0 DCDC1 */
|
||||
#define AXP192_DCDC13_LDO23_CONTROL (0x12)
|
||||
#define AXP192_DCDC2_VOLTAGE (0x23)
|
||||
#define AXP192_DCDC2_SLOPE (0x25)
|
||||
#define AXP192_DCDC1_VOLTAGE (0x26)
|
||||
#define AXP192_DCDC3_VOLTAGE (0x27)
|
||||
/* Output voltage control: 7-4 LDO2, 3-0 LDO3 */
|
||||
#define AXP192_LDO23_VOLTAGE (0x28)
|
||||
#define AXP192_VBUS_IPSOUT_CHANNEL (0x30)
|
||||
#define AXP192_SHUTDOWN_VOLTAGE (0x31)
|
||||
#define AXP192_SHUTDOWN_BATTERY_CHGLED_CONTROL (0x32)
|
||||
#define AXP192_CHARGE_CONTROL_1 (0x33)
|
||||
#define AXP192_CHARGE_CONTROL_2 (0x34)
|
||||
#define AXP192_BATTERY_CHARGE_CONTROL (0x35)
|
||||
#define AXP192_PEK (0x36)
|
||||
#define AXP192_DCDC_FREQUENCY (0x37)
|
||||
#define AXP192_BATTERY_CHARGE_LOW_TEMP (0x38)
|
||||
#define AXP192_BATTERY_CHARGE_HIGH_TEMP (0x39)
|
||||
#define AXP192_APS_LOW_POWER1 (0x3A)
|
||||
#define AXP192_APS_LOW_POWER2 (0x3B)
|
||||
#define AXP192_BATTERY_DISCHARGE_LOW_TEMP (0x3c)
|
||||
#define AXP192_BATTERY_DISCHARGE_HIGH_TEMP (0x3d)
|
||||
#define AXP192_DCDC_MODE (0x80)
|
||||
#define AXP192_ADC_ENABLE_1 (0x82)
|
||||
#define AXP192_ADC_ENABLE_2 (0x83)
|
||||
#define AXP192_ADC_RATE_TS_PIN (0x84)
|
||||
#define AXP192_GPIO30_INPUT_RANGE (0x85)
|
||||
#define AXP192_GPIO0_ADC_IRQ_RISING (0x86)
|
||||
#define AXP192_GPIO0_ADC_IRQ_FALLING (0x87)
|
||||
#define AXP192_TIMER_CONTROL (0x8a)
|
||||
#define AXP192_VBUS_MONITOR (0x8b)
|
||||
#define AXP192_TEMP_SHUTDOWN_CONTROL (0x8f)
|
||||
|
||||
/* GPIO control registers */
|
||||
#define AXP192_GPIO0_CONTROL (0x90)
|
||||
#define AXP192_GPIO0_LDOIO0_VOLTAGE (0x91)
|
||||
#define AXP192_GPIO1_CONTROL (0x92)
|
||||
#define AXP192_GPIO2_CONTROL (0x93)
|
||||
#define AXP192_GPIO20_SIGNAL_STATUS (0x94)
|
||||
#define AXP192_GPIO43_FUNCTION_CONTROL (0x95)
|
||||
#define AXP192_GPIO43_SIGNAL_STATUS (0x96)
|
||||
#define AXP192_GPIO20_PULLDOWN_CONTROL (0x97)
|
||||
#define AXP192_PWM1_FREQUENCY (0x98)
|
||||
#define AXP192_PWM1_DUTY_CYCLE_1 (0x99)
|
||||
#define AXP192_PWM1_DUTY_CYCLE_2 (0x9a)
|
||||
#define AXP192_PWM2_FREQUENCY (0x9b)
|
||||
#define AXP192_PWM2_DUTY_CYCLE_1 (0x9c)
|
||||
#define AXP192_PWM2_DUTY_CYCLE_2 (0x9d)
|
||||
#define AXP192_N_RSTO_GPIO5_CONTROL (0x9e)
|
||||
|
||||
/* Interrupt control registers */
|
||||
#define AXP192_ENABLE_CONTROL_1 (0x40)
|
||||
#define AXP192_ENABLE_CONTROL_2 (0x41)
|
||||
#define AXP192_ENABLE_CONTROL_3 (0x42)
|
||||
#define AXP192_ENABLE_CONTROL_4 (0x43)
|
||||
#define AXP192_ENABLE_CONTROL_5 (0x4a)
|
||||
#define AXP192_IRQ_STATUS_1 (0x44)
|
||||
#define AXP192_IRQ_STATUS_2 (0x45)
|
||||
#define AXP192_IRQ_STATUS_3 (0x46)
|
||||
#define AXP192_IRQ_STATUS_4 (0x47)
|
||||
#define AXP192_IRQ_STATUS_5 (0x4d)
|
||||
|
||||
/* ADC data registers */
|
||||
#define AXP192_ACIN_VOLTAGE (0x56)
|
||||
#define AXP192_ACIN_CURRENT (0x58)
|
||||
#define AXP192_VBUS_VOLTAGE (0x5a)
|
||||
#define AXP192_VBUS_CURRENT (0x5c)
|
||||
#define AXP192_TEMP (0x5e)
|
||||
#define AXP192_TS_INPUT (0x62)
|
||||
#define AXP192_GPIO0_VOLTAGE (0x64)
|
||||
#define AXP192_GPIO1_VOLTAGE (0x66)
|
||||
#define AXP192_GPIO2_VOLTAGE (0x68)
|
||||
#define AXP192_GPIO3_VOLTAGE (0x6a)
|
||||
#define AXP192_BATTERY_POWER (0x70)
|
||||
#define AXP192_BATTERY_VOLTAGE (0x78)
|
||||
#define AXP192_CHARGE_CURRENT (0x7a)
|
||||
#define AXP192_DISCHARGE_CURRENT (0x7c)
|
||||
#define AXP192_APS_VOLTAGE (0x7e)
|
||||
#define AXP192_CHARGE_COULOMB (0xb0)
|
||||
#define AXP192_DISCHARGE_COULOMB (0xb4)
|
||||
#define AXP192_COULOMB_COUNTER_CONTROL (0xb8)
|
||||
|
||||
/* Computed ADC */
|
||||
#define AXP192_COULOMB_COUNTER (0xff)
|
||||
|
||||
/* IOCTL commands */
|
||||
#define AXP192_READ_POWER_STATUS (0x0001)
|
||||
#define AXP192_READ_CHARGE_STATUS (0x0101)
|
||||
|
||||
#define AXP192_COULOMB_COUNTER_ENABLE (0xb801)
|
||||
#define AXP192_COULOMB_COUNTER_DISABLE (0xb802)
|
||||
#define AXP192_COULOMB_COUNTER_SUSPEND (0xb803)
|
||||
#define AXP192_COULOMB_COUNTER_CLEAR (0xb804)
|
||||
|
||||
#define AXP192_LDOIO0_ENABLE (0x9000)
|
||||
#define AXP192_LDOIO0_DISABLE (0x9001)
|
||||
|
||||
#define AXP192_DCDC2_ENABLE (0x1000)
|
||||
#define AXP192_DCDC2_DISABLE (0x1001)
|
||||
#define AXP192_EXTEN_ENABLE (0x1002)
|
||||
#define AXP192_EXTEN_DISABLE (0x1003)
|
||||
|
||||
#define AXP192_LDO2_ENABLE (0x1200)
|
||||
#define AXP192_LDO2_DISABLE (0x1201)
|
||||
#define AXP192_LDO3_ENABLE (0x1202)
|
||||
#define AXP192_LDO3_DISABLE (0x1203)
|
||||
#define AXP192_DCDC1_ENABLE (0x1204)
|
||||
#define AXP192_DCDC1_DISABLE (0x1205)
|
||||
#define AXP192_DCDC3_ENABLE (0x1206)
|
||||
#define AXP192_DCDC3_DISABLE (0x1207)
|
||||
|
||||
#define AXP192_DCDC1_SET_VOLTAGE (0x2600)
|
||||
#define AXP192_DCDC2_SET_VOLTAGE (0x2300)
|
||||
#define AXP192_DCDC3_SET_VOLTAGE (0x2700)
|
||||
#define AXP192_LDO2_SET_VOLTAGE (0x2800)
|
||||
#define AXP192_LDO3_SET_VOLTAGE (0x2801)
|
||||
#define AXP192_LDOIO0_SET_VOLTAGE (0x9100)
|
||||
|
||||
#define AXP192_LOW (0)
|
||||
#define AXP192_HIGH (1)
|
||||
|
||||
#define AXP192_GPIO0_SET_LEVEL (0x9400)
|
||||
#define AXP192_GPIO1_SET_LEVEL (0x9401)
|
||||
#define AXP192_GPIO2_SET_LEVEL (0x9402)
|
||||
#define AXP192_GPIO4_SET_LEVEL (0x9601)
|
||||
|
||||
/* Error codes */
|
||||
#define AXP192_OK (0)
|
||||
#define AXP192_ERROR_NOTTY (-1)
|
||||
#define AXP192_ERROR_EINVAL (-22)
|
||||
#define AXP192_ERROR_ENOTSUP (-95)
|
||||
|
||||
typedef struct {
|
||||
uint8_t command;
|
||||
uint8_t data[2];
|
||||
uint8_t count;
|
||||
} axp192_init_command_t;
|
||||
|
||||
/* These should be provided by the HAL. */
|
||||
typedef struct {
|
||||
int32_t (* read)(void *handle, uint8_t address, uint8_t reg, uint8_t *buffer, uint16_t size);
|
||||
int32_t (* write)(void *handle, uint8_t address, uint8_t reg, const uint8_t *buffer, uint16_t size);
|
||||
void *handle;
|
||||
} axp192_t;
|
||||
|
||||
typedef int32_t axp192_err_t;
|
||||
|
||||
axp192_err_t axp192_init(const axp192_t *axp);
|
||||
axp192_err_t axp192_read(const axp192_t *axp, uint8_t reg, void *buffer);
|
||||
axp192_err_t axp192_write(const axp192_t *axp, uint8_t reg, uint8_t value);
|
||||
axp192_err_t axp192_ioctl(const axp192_t *axp, int command, ...);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2021 Mika Tuupola
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,4 +0,0 @@
|
||||
From https://github.com/tuupola/axp192
|
||||
|
||||
Changed some code for Tactility:
|
||||
- `axp192_write()` now accepts a byte value instead of a byte pointer
|
||||
@@ -1,125 +0,0 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2021 Mika Tuupola
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-cut-
|
||||
|
||||
This file is part of hardware agnostic I2C driver for AXP192:
|
||||
https://github.com/tuupola/axp192
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
Version: 0.6.0
|
||||
|
||||
*/
|
||||
|
||||
#ifndef _AXP192_CONFIG_H
|
||||
#define _AXP192_CONFIG_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifdef AXP192_INCLUDE_SDKCONFIG_H
|
||||
#include "sdkconfig.h"
|
||||
|
||||
/* This requires you to run menuconfig first. */
|
||||
|
||||
#define CONFIG_AXP192_EXTEN_DCDC2_CONTROL ( \
|
||||
CONFIG_AXP192_EXTEN_DCDC2_CONTROL_BIT2 | \
|
||||
CONFIG_AXP192_EXTEN_DCDC2_CONTROL_BIT0 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_DCDC13_LDO23_CONTROL ( \
|
||||
CONFIG_AXP192_DCDC13_LDO23_CONTROL_BIT6 | \
|
||||
CONFIG_AXP192_DCDC13_LDO23_CONTROL_BIT4 | \
|
||||
CONFIG_AXP192_DCDC13_LDO23_CONTROL_BIT3 | \
|
||||
CONFIG_AXP192_DCDC13_LDO23_CONTROL_BIT2 | \
|
||||
CONFIG_AXP192_DCDC13_LDO23_CONTROL_BIT1 | \
|
||||
CONFIG_AXP192_DCDC13_LDO23_CONTROL_BIT0 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_LDO23_VOLTAGE ( \
|
||||
CONFIG_AXP192_LDO23_VOLTAGE_BIT74 | \
|
||||
CONFIG_AXP192_LDO23_VOLTAGE_BIT30 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_DCDC1_VOLTAGE ( \
|
||||
CONFIG_AXP192_DCDC1_VOLTAGE_BIT60 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_DCDC3_VOLTAGE ( \
|
||||
CONFIG_AXP192_DCDC3_VOLTAGE_BIT60 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_ADC_ENABLE_1 ( \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT7 | \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT6 | \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT5 | \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT4 | \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT3 | \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT2 | \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT1 | \
|
||||
CONFIG_AXP192_ADC_ENABLE_1_BIT0 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_CHARGE_CONTROL_1 ( \
|
||||
CONFIG_AXP192_CHARGE_CONTROL_1_BIT7 | \
|
||||
CONFIG_AXP192_CHARGE_CONTROL_1_BIT65 | \
|
||||
CONFIG_AXP192_CHARGE_CONTROL_1_BIT4 | \
|
||||
CONFIG_AXP192_CHARGE_CONTROL_1_BIT30 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_BATTERY_CHARGE_CONTROL ( \
|
||||
CONFIG_AXP192_BATTERY_CHARGE_CONTROL_BIT7 | \
|
||||
CONFIG_AXP192_BATTERY_CHARGE_CONTROL_BIT65 | \
|
||||
CONFIG_AXP192_BATTERY_CHARGE_CONTROL_BIT10 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_GPIO0_CONTROL ( \
|
||||
CONFIG_AXP192_GPIO0_CONTROL_BIT20 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_GPIO1_CONTROL ( \
|
||||
CONFIG_AXP192_GPIO1_CONTROL_BIT20 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_GPIO2_CONTROL ( \
|
||||
CONFIG_AXP192_GPIO2_CONTROL_BIT20 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_GPIO43_FUNCTION_CONTROL ( \
|
||||
CONFIG_AXP192_GPIO43_FUNCTION_CONTROL_BIT7 | \
|
||||
CONFIG_AXP192_GPIO43_FUNCTION_CONTROL_BIT32 | \
|
||||
CONFIG_AXP192_GPIO43_FUNCTION_CONTROL_BIT10 \
|
||||
)
|
||||
|
||||
#define CONFIG_AXP192_GPIO0_LDOIO0_VOLTAGE ( \
|
||||
CONFIG_AXP192_GPIO0_LDOIO0_VOLTAGE_BIT74 \
|
||||
)
|
||||
|
||||
#endif /* AXP192_INCLUDE_SDKCONFIG_H */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -1,5 +0,0 @@
|
||||
# AXP192
|
||||
|
||||
Power management with I2C interface. Used on M5Stack Core2 and StickCPlus.
|
||||
|
||||
It includes driver code from https://github.com/tuupola/axp192, Copyright (c) 2019-2021 Mika Tuupola, MIT License
|
||||
@@ -1,118 +0,0 @@
|
||||
#include "Axp192.h"
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
|
||||
constexpr auto TAG = "Axp192Power";
|
||||
|
||||
int32_t Axp192::i2cRead(void* handle, uint8_t address, uint8_t reg, uint8_t* buffer, uint16_t size) {
|
||||
const auto* device = static_cast<Axp192*>(handle);
|
||||
if (i2c_controller_read_register(device->configuration->controller, address, reg, buffer, size, device->configuration->readTimeout) == ERROR_NONE) {
|
||||
return AXP192_OK;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t Axp192::i2cWrite(void* handle, uint8_t address, uint8_t reg, const uint8_t* buffer, uint16_t size) {
|
||||
const auto* device = static_cast<Axp192*>(handle);
|
||||
if (i2c_controller_write_register(device->configuration->controller, address, reg, buffer, size, device->configuration->writeTimeout) == ERROR_NONE) {
|
||||
return AXP192_OK;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool Axp192::supportsMetric(MetricType type) const {
|
||||
if (!isInitialized) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
using enum MetricType;
|
||||
case BatteryVoltage:
|
||||
case ChargeLevel:
|
||||
case IsCharging:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Axp192::getMetric(MetricType type, MetricData& data) {
|
||||
switch (type) {
|
||||
using enum MetricType;
|
||||
case BatteryVoltage: {
|
||||
float voltage;
|
||||
if (axp192_read(&axpDevice, AXP192_BATTERY_VOLTAGE, &voltage) == ESP_OK) {
|
||||
data.valueAsUint32 = (uint32_t)std::max((voltage * 1000.f), 0.0f);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case ChargeLevel: {
|
||||
float vbat, charge_current;
|
||||
if (
|
||||
axp192_read(&axpDevice, AXP192_BATTERY_VOLTAGE, &vbat) == ESP_OK &&
|
||||
axp192_read(&axpDevice, AXP192_CHARGE_CURRENT, &charge_current) == ESP_OK
|
||||
) {
|
||||
float max_voltage = 4.20f;
|
||||
float min_voltage = 2.69f; // From M5Unified
|
||||
float voltage_correction = (charge_current > 0.01f) ? -0.1f : 0.f; // Roughly 0.1V drop when ccharging
|
||||
float corrected_voltage = vbat + voltage_correction;
|
||||
if (corrected_voltage > 2.69f) {
|
||||
float charge_factor = (corrected_voltage - min_voltage) / (max_voltage - min_voltage);
|
||||
data.valueAsUint8 = (uint8_t)(charge_factor * 100.f);
|
||||
} else {
|
||||
data.valueAsUint8 = 0;
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case IsCharging: {
|
||||
float charge_current;
|
||||
if (axp192_read(&axpDevice, AXP192_CHARGE_CURRENT, &charge_current) == ESP_OK) {
|
||||
data.valueAsBool = charge_current > 0.001f;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
case Current: {
|
||||
float charge_current, discharge_current;
|
||||
if (
|
||||
axp192_read(&axpDevice, AXP192_CHARGE_CURRENT, &charge_current) == ESP_OK &&
|
||||
axp192_read(&axpDevice, AXP192_DISCHARGE_CURRENT, &discharge_current) == ESP_OK
|
||||
) {
|
||||
if (charge_current > 0.0f) {
|
||||
data.valueAsInt32 = (int32_t) (charge_current * 1000.0f);
|
||||
} else {
|
||||
data.valueAsInt32 = -(int32_t) (discharge_current * 1000.0f);
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Axp192::isAllowedToCharge() const {
|
||||
uint8_t buffer;
|
||||
if (axp192_read(&axpDevice, AXP192_CHARGE_CONTROL_1, &buffer) == ESP_OK) {
|
||||
return buffer & 0x80;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void Axp192::setAllowedToCharge(bool canCharge) {
|
||||
uint8_t buffer;
|
||||
if (axp192_read(&axpDevice, AXP192_CHARGE_CONTROL_1, &buffer) == ESP_OK) {
|
||||
buffer = (buffer & 0x7F) + (canCharge ? 0x80 : 0x00);
|
||||
axp192_write(&axpDevice, AXP192_CHARGE_CONTROL_1, buffer);
|
||||
}
|
||||
}
|
||||
@@ -1,440 +0,0 @@
|
||||
/*
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2021 Mika Tuupola
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
-cut-
|
||||
|
||||
This file is part of hardware agnostic I2C driver for AXP192:
|
||||
https://github.com/tuupola/axp192
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
Version: 0.6.0
|
||||
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "axp192/axp192_config.h"
|
||||
#include "axp192/axp192.h"
|
||||
|
||||
static axp192_err_t read_coloumb_counter(const axp192_t *axp, float *buffer);
|
||||
static axp192_err_t read_battery_power(const axp192_t *axp, float *buffer);
|
||||
|
||||
static const axp192_init_command_t init_commands[] = {
|
||||
#ifdef AXP192_INCLUDE_SDKCONFIG_H
|
||||
/* Currently you have to use menuconfig to be able to use axp192_init() */
|
||||
{AXP192_DCDC1_VOLTAGE, {CONFIG_AXP192_DCDC1_VOLTAGE}, 1},
|
||||
{AXP192_DCDC3_VOLTAGE, {CONFIG_AXP192_DCDC3_VOLTAGE}, 1},
|
||||
{AXP192_LDO23_VOLTAGE, {CONFIG_AXP192_LDO23_VOLTAGE}, 1},
|
||||
{AXP192_GPIO0_LDOIO0_VOLTAGE, {CONFIG_AXP192_GPIO0_LDOIO0_VOLTAGE}, 1},
|
||||
{AXP192_DCDC13_LDO23_CONTROL, {CONFIG_AXP192_DCDC13_LDO23_CONTROL}, 1},
|
||||
{AXP192_EXTEN_DCDC2_CONTROL, {CONFIG_AXP192_EXTEN_DCDC2_CONTROL}, 1},
|
||||
{AXP192_GPIO0_CONTROL, {CONFIG_AXP192_GPIO0_CONTROL}, 1},
|
||||
{AXP192_GPIO1_CONTROL, {CONFIG_AXP192_GPIO1_CONTROL}, 1},
|
||||
{AXP192_GPIO2_CONTROL, {CONFIG_AXP192_GPIO2_CONTROL}, 1},
|
||||
{AXP192_GPIO43_FUNCTION_CONTROL, {CONFIG_AXP192_GPIO43_FUNCTION_CONTROL}, 1},
|
||||
{AXP192_ADC_ENABLE_1, {CONFIG_AXP192_ADC_ENABLE_1}, 1},
|
||||
{AXP192_CHARGE_CONTROL_1, {CONFIG_AXP192_CHARGE_CONTROL_1}, 1},
|
||||
{AXP192_BATTERY_CHARGE_CONTROL, {CONFIG_AXP192_BATTERY_CHARGE_CONTROL}, 1},
|
||||
#endif /* AXP192_INCLUDE_SDKCONFIG_H */
|
||||
/* End of commands. */
|
||||
{0, {0}, 0xff},
|
||||
};
|
||||
|
||||
axp192_err_t axp192_init(const axp192_t *axp)
|
||||
{
|
||||
uint8_t cmd = 0;
|
||||
axp192_err_t status;
|
||||
|
||||
/* Send all the commands. */
|
||||
while (init_commands[cmd].count != 0xff) {
|
||||
status = axp->write(
|
||||
axp->handle,
|
||||
AXP192_ADDRESS,
|
||||
init_commands[cmd].command,
|
||||
init_commands[cmd].data,
|
||||
init_commands[cmd].count & 0x1f
|
||||
);
|
||||
if (AXP192_OK != status) {
|
||||
return status;
|
||||
}
|
||||
cmd++;
|
||||
}
|
||||
|
||||
return AXP192_OK;
|
||||
}
|
||||
|
||||
static axp192_err_t axp192_read_adc(const axp192_t *axp, uint8_t reg, float *buffer)
|
||||
{
|
||||
uint8_t tmp[4];
|
||||
float sensitivity = 1.0;
|
||||
float offset = 0.0;
|
||||
axp192_err_t status;
|
||||
|
||||
switch (reg) {
|
||||
case AXP192_ACIN_VOLTAGE:
|
||||
case AXP192_VBUS_VOLTAGE:
|
||||
/* 1.7mV per LSB */
|
||||
sensitivity = 1.7 / 1000;
|
||||
break;
|
||||
case AXP192_ACIN_CURRENT:
|
||||
/* 0.375mA per LSB */
|
||||
sensitivity = 0.625 / 1000;
|
||||
break;
|
||||
case AXP192_VBUS_CURRENT:
|
||||
/* 0.375mA per LSB */
|
||||
sensitivity = 0.375 / 1000;
|
||||
break;
|
||||
case AXP192_TEMP:
|
||||
/* 0.1C per LSB, 0x00 = -144.7C */
|
||||
sensitivity = 0.1;
|
||||
offset = -144.7;
|
||||
break;
|
||||
case AXP192_TS_INPUT:
|
||||
/* 0.8mV per LSB */
|
||||
sensitivity = 0.8 / 1000;
|
||||
break;
|
||||
case AXP192_BATTERY_POWER:
|
||||
/* 1.1mV * 0.5mA per LSB */
|
||||
return read_battery_power(axp, buffer);
|
||||
break;
|
||||
case AXP192_BATTERY_VOLTAGE:
|
||||
/* 1.1mV per LSB */
|
||||
sensitivity = 1.1 / 1000;
|
||||
break;
|
||||
case AXP192_CHARGE_CURRENT:
|
||||
case AXP192_DISCHARGE_CURRENT:
|
||||
/* 0.5mV per LSB */
|
||||
sensitivity = 0.5 / 1000;
|
||||
break;
|
||||
case AXP192_APS_VOLTAGE:
|
||||
/* 1.4mV per LSB */
|
||||
sensitivity = 1.4 / 1000;
|
||||
break;
|
||||
case AXP192_COULOMB_COUNTER:
|
||||
/* This is currently untested. */
|
||||
return read_coloumb_counter(axp, buffer);
|
||||
break;
|
||||
}
|
||||
|
||||
status = axp->read(axp->handle, AXP192_ADDRESS, reg, tmp, 2);
|
||||
if (AXP192_OK != status) {
|
||||
return status;
|
||||
}
|
||||
*buffer = (((tmp[0] << 4) + tmp[1]) * sensitivity) + offset;
|
||||
|
||||
return AXP192_OK;
|
||||
}
|
||||
|
||||
axp192_err_t axp192_read(const axp192_t *axp, uint8_t reg, void *buffer) {
|
||||
switch (reg) {
|
||||
case AXP192_ACIN_VOLTAGE:
|
||||
case AXP192_VBUS_VOLTAGE:
|
||||
case AXP192_ACIN_CURRENT:
|
||||
case AXP192_VBUS_CURRENT:
|
||||
case AXP192_TEMP:
|
||||
case AXP192_TS_INPUT:
|
||||
case AXP192_BATTERY_POWER:
|
||||
case AXP192_BATTERY_VOLTAGE:
|
||||
case AXP192_CHARGE_CURRENT:
|
||||
case AXP192_DISCHARGE_CURRENT:
|
||||
case AXP192_APS_VOLTAGE:
|
||||
case AXP192_COULOMB_COUNTER:
|
||||
/* Return ADC value. */
|
||||
return axp192_read_adc(axp, reg, buffer);
|
||||
break;
|
||||
default:
|
||||
/* Return raw register value. */
|
||||
return axp->read(axp->handle, AXP192_ADDRESS, reg, buffer, 1);
|
||||
}
|
||||
}
|
||||
|
||||
axp192_err_t axp192_write(const axp192_t *axp, uint8_t reg, uint8_t value) {
|
||||
switch (reg) {
|
||||
case AXP192_ACIN_VOLTAGE:
|
||||
case AXP192_VBUS_VOLTAGE:
|
||||
case AXP192_ACIN_CURRENT:
|
||||
case AXP192_VBUS_CURRENT:
|
||||
case AXP192_TEMP:
|
||||
case AXP192_TS_INPUT:
|
||||
case AXP192_BATTERY_POWER:
|
||||
case AXP192_BATTERY_VOLTAGE:
|
||||
case AXP192_CHARGE_CURRENT:
|
||||
case AXP192_DISCHARGE_CURRENT:
|
||||
case AXP192_APS_VOLTAGE:
|
||||
case AXP192_COULOMB_COUNTER:
|
||||
/* Read only register. */
|
||||
return AXP192_ERROR_ENOTSUP;
|
||||
break;
|
||||
default:
|
||||
/* Write raw register value. */
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &value, 1);
|
||||
}
|
||||
}
|
||||
|
||||
axp192_err_t axp192_ioctl(const axp192_t *axp, int command, ...)
|
||||
{
|
||||
uint8_t reg = command >> 8;
|
||||
uint8_t tmp;
|
||||
uint16_t argument;
|
||||
va_list ap;
|
||||
|
||||
switch (command) {
|
||||
case AXP192_COULOMB_COUNTER_ENABLE:
|
||||
tmp = 0b10000000;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_COULOMB_COUNTER_DISABLE:
|
||||
tmp = 0b00000000;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_COULOMB_COUNTER_SUSPEND:
|
||||
tmp = 0b11000000;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_COULOMB_COUNTER_CLEAR:
|
||||
tmp = 0b10100000;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_LDOIO0_ENABLE:
|
||||
/* 0x02 = LDO */
|
||||
tmp = 0b00000010;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_LDOIO0_DISABLE:
|
||||
/* 0x07 = float */
|
||||
tmp = 0b00000111;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_LDO2_ENABLE:
|
||||
/* This is currently untested. */
|
||||
case AXP192_EXTEN_ENABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp |= 0b00000100;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_LDO2_DISABLE:
|
||||
/* This is currently untested. */
|
||||
case AXP192_EXTEN_DISABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp &= ~0b00000100;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_GPIO2_SET_LEVEL:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
va_start(ap, command);
|
||||
argument = (uint8_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
if (argument) {
|
||||
tmp |= 0b00000100;
|
||||
} else {
|
||||
tmp &= ~0b00000100;
|
||||
}
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_LDO3_ENABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp |= 0b00001000;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_LDO3_DISABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp &= ~0b00001000;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_DCDC1_ENABLE:
|
||||
/* This is currently untested. */
|
||||
case AXP192_DCDC2_ENABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp |= 0b00000001;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_DCDC1_DISABLE:
|
||||
/* This is currently untested. */
|
||||
case AXP192_DCDC2_DISABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp &= ~0b00000001;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_DCDC3_ENABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp |= 0b00000010;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_DCDC3_DISABLE:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
tmp &= ~0b00000010;
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_GPIO1_SET_LEVEL:
|
||||
case AXP192_GPIO4_SET_LEVEL:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
va_start(ap, command);
|
||||
argument = (uint8_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
if (argument) {
|
||||
tmp |= 0b00000010;
|
||||
} else {
|
||||
tmp &= ~0b00000010;
|
||||
}
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_GPIO0_SET_LEVEL:
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
va_start(ap, command);
|
||||
argument = (uint8_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
if (argument) {
|
||||
tmp |= 0b00000001;
|
||||
} else {
|
||||
tmp &= ~0b00000001;
|
||||
}
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
case AXP192_DCDC1_SET_VOLTAGE:
|
||||
case AXP192_DCDC3_SET_VOLTAGE:
|
||||
va_start(ap, command);
|
||||
argument = (uint16_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
|
||||
/* 700-3500mv 25mV per step */
|
||||
if ((argument < 700) || (argument > 3500)) {
|
||||
return AXP192_ERROR_EINVAL;
|
||||
}
|
||||
tmp = (argument - 700) / 25;
|
||||
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_DCDC2_SET_VOLTAGE:
|
||||
va_start(ap, command);
|
||||
argument = (uint16_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
|
||||
/* 700-2275mV 25mV per step */
|
||||
if ((argument < 700) || (argument > 2275)) {
|
||||
return AXP192_ERROR_EINVAL;
|
||||
}
|
||||
tmp = (argument - 700) / 25;
|
||||
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_LDO2_SET_VOLTAGE:
|
||||
va_start(ap, command);
|
||||
argument = (uint16_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
|
||||
/* 1800-3300mV 100mV per step */
|
||||
if ((argument < 1800) || (argument > 3300)) {
|
||||
return AXP192_ERROR_EINVAL;
|
||||
}
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
|
||||
tmp &= ~0xf0;
|
||||
tmp |= (((argument - 1800) / 100) << 4);
|
||||
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_LDO3_SET_VOLTAGE:
|
||||
va_start(ap, command);
|
||||
argument = (uint16_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
|
||||
/* 1800-3300mV 100mV per step */
|
||||
if ((argument < 1800) || (argument > 3300)) {
|
||||
return AXP192_ERROR_EINVAL;
|
||||
}
|
||||
axp->read(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
|
||||
tmp &= ~0x0f;
|
||||
tmp |= ((argument - 1800) / 100);
|
||||
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
/* This is currently untested. */
|
||||
case AXP192_LDOIO0_SET_VOLTAGE:
|
||||
va_start(ap, command);
|
||||
argument = (uint16_t) va_arg(ap, int);
|
||||
va_end(ap);
|
||||
|
||||
/* 1800-3300mV 100mV per step, 2800mV default. */
|
||||
if ((argument < 1800) || (argument > 3300)) {
|
||||
return AXP192_ERROR_EINVAL;
|
||||
}
|
||||
tmp = (((argument - 1800) / 100) << 4);
|
||||
|
||||
return axp->write(axp->handle, AXP192_ADDRESS, reg, &tmp, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
return AXP192_ERROR_NOTTY;
|
||||
}
|
||||
|
||||
static axp192_err_t read_coloumb_counter(const axp192_t *axp, float *buffer)
|
||||
{
|
||||
uint8_t tmp[4];
|
||||
int32_t coin, coout;
|
||||
axp192_err_t status;
|
||||
|
||||
status = axp->read(axp->handle, AXP192_ADDRESS, AXP192_CHARGE_COULOMB, tmp, sizeof(coin));
|
||||
if (AXP192_OK != status) {
|
||||
return status;
|
||||
}
|
||||
coin = (tmp[0] << 24) + (tmp[1] << 16) + (tmp[2] << 8) + tmp[3];
|
||||
|
||||
status = axp->read(axp->handle, AXP192_ADDRESS, AXP192_DISCHARGE_COULOMB, tmp, sizeof(coout));
|
||||
if (AXP192_OK != status) {
|
||||
return status;
|
||||
}
|
||||
coout = (tmp[0] << 24) + (tmp[1] << 16) + (tmp[2] << 8) + tmp[3];
|
||||
|
||||
/* CmAh = 65536 * 0.5mA *(coin - cout) / 3600 / ADC sample rate */
|
||||
*buffer = 32768 * (coin - coout) / 3600 / 25;
|
||||
|
||||
return AXP192_OK;
|
||||
}
|
||||
|
||||
static axp192_err_t read_battery_power(const axp192_t *axp, float *buffer)
|
||||
{
|
||||
uint8_t tmp[4];
|
||||
float sensitivity;
|
||||
axp192_err_t status;
|
||||
|
||||
/* 1.1mV * 0.5mA per LSB */
|
||||
sensitivity = 1.1 * 0.5 / 1000;
|
||||
status = axp->read(axp->handle, AXP192_ADDRESS, AXP192_BATTERY_POWER, tmp, 3);
|
||||
if (AXP192_OK != status) {
|
||||
return status;
|
||||
}
|
||||
*buffer = (((tmp[0] << 16) + (tmp[1] << 8) + tmp[2]) * sensitivity);
|
||||
return AXP192_OK;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
idf_component_register(
|
||||
SRC_DIRS "Source"
|
||||
INCLUDE_DIRS "Source"
|
||||
REQUIRES Tactility esp_adc
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
# EstimatedPower
|
||||
|
||||
Use ADC measurements to read voltage and estimate the available power that is left in the battery.
|
||||
@@ -1,65 +0,0 @@
|
||||
#include "ChargeFromAdcVoltage.h"
|
||||
#include <tactility/log.h>
|
||||
|
||||
constexpr auto* TAG = "ChargeFromAdcV";
|
||||
constexpr auto MAX_VOLTAGE_SAMPLES = 15;
|
||||
|
||||
ChargeFromAdcVoltage::ChargeFromAdcVoltage(
|
||||
const Configuration& configuration,
|
||||
float voltageMin,
|
||||
float voltageMax
|
||||
) : configuration(configuration), chargeFromVoltage(voltageMin, voltageMax) {
|
||||
if (adc_oneshot_new_unit(&configuration.adcConfig, &adcHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "ADC config failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (adc_oneshot_config_channel(adcHandle, configuration.adcChannel, &configuration.adcChannelConfig) != ESP_OK) {
|
||||
LOG_E(TAG, "ADC channel config failed");
|
||||
|
||||
adc_oneshot_del_unit(adcHandle);
|
||||
adcHandle = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ChargeFromAdcVoltage::~ChargeFromAdcVoltage() {
|
||||
if (adcHandle) {
|
||||
adc_oneshot_del_unit(adcHandle);
|
||||
}
|
||||
}
|
||||
|
||||
bool ChargeFromAdcVoltage::readBatteryVoltageOnce(uint32_t& output) const {
|
||||
if (adcHandle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
int raw;
|
||||
if (adc_oneshot_read(adcHandle, configuration.adcChannel, &raw) == ESP_OK) {
|
||||
output = configuration.adcMultiplier * ((1000.f * configuration.adcRefVoltage) / 4096.f) * (float)raw;
|
||||
LOG_V(TAG, "Raw = %d, voltage = %u", raw, (unsigned)output);
|
||||
return true;
|
||||
} else {
|
||||
LOG_E(TAG, "Read failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool ChargeFromAdcVoltage::readBatteryVoltageSampled(uint32_t& output) const {
|
||||
size_t samples_read = 0;
|
||||
uint32_t sample_accumulator = 0;
|
||||
uint32_t sample_read_buffer;
|
||||
|
||||
for (size_t i = 0; i < MAX_VOLTAGE_SAMPLES; ++i) {
|
||||
if (readBatteryVoltageOnce(sample_read_buffer)) {
|
||||
sample_accumulator += sample_read_buffer;
|
||||
samples_read++;
|
||||
}
|
||||
}
|
||||
|
||||
if (samples_read == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
output = sample_accumulator / samples_read;
|
||||
return true;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <ChargeFromVoltage.h>
|
||||
#include <esp_adc/adc_oneshot.h>
|
||||
|
||||
class ChargeFromAdcVoltage {
|
||||
|
||||
public:
|
||||
|
||||
struct Configuration {
|
||||
float adcMultiplier = 1.0f;
|
||||
float adcRefVoltage = 3.3f;
|
||||
adc_channel_t adcChannel = ADC_CHANNEL_3;
|
||||
adc_oneshot_unit_init_cfg_t adcConfig = {
|
||||
.unit_id = ADC_UNIT_1,
|
||||
.clk_src = ADC_RTC_CLK_SRC_DEFAULT,
|
||||
.ulp_mode = ADC_ULP_MODE_DISABLE,
|
||||
};
|
||||
adc_oneshot_chan_cfg_t adcChannelConfig = {
|
||||
.atten = ADC_ATTEN_DB_12,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
};
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
adc_oneshot_unit_handle_t adcHandle = nullptr;
|
||||
Configuration configuration;
|
||||
ChargeFromVoltage chargeFromVoltage;
|
||||
|
||||
public:
|
||||
|
||||
explicit ChargeFromAdcVoltage(const Configuration& configuration, float voltageMin = 3.2f, float voltageMax = 4.2f);
|
||||
|
||||
~ChargeFromAdcVoltage();
|
||||
|
||||
bool isInitialized() const { return adcHandle != nullptr; }
|
||||
|
||||
bool readBatteryVoltageSampled(uint32_t& output) const;
|
||||
|
||||
bool readBatteryVoltageOnce(uint32_t& output) const;
|
||||
|
||||
uint8_t estimateChargeLevelFromVoltage(uint32_t milliVolt) const { return chargeFromVoltage.estimateCharge(milliVolt); }
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
#include "ChargeFromVoltage.h"
|
||||
|
||||
#include <tactility/log.h>
|
||||
#include <algorithm>
|
||||
|
||||
constexpr auto* TAG = "ChargeFromVoltage";
|
||||
|
||||
uint8_t ChargeFromVoltage::estimateCharge(uint32_t milliVolt) const {
|
||||
const float volts = std::min((float)milliVolt / 1000.f, batteryVoltageMax);
|
||||
if (volts < batteryVoltageMin) {
|
||||
return 0;
|
||||
}
|
||||
const float voltage_percentage = (volts - batteryVoltageMin) / (batteryVoltageMax - batteryVoltageMin);
|
||||
const float voltage_factor = std::min(1.0f, voltage_percentage);
|
||||
const auto charge_level = (uint8_t) (voltage_factor * 100.f);
|
||||
LOG_D(TAG, "mV = %u, scaled = %f, factor = %.2f, result = %d", (unsigned)milliVolt, volts, voltage_factor, charge_level);
|
||||
return charge_level;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
class ChargeFromVoltage {
|
||||
|
||||
float batteryVoltageMin;
|
||||
float batteryVoltageMax;
|
||||
|
||||
public:
|
||||
|
||||
explicit ChargeFromVoltage(float voltageMin = 3.2f, float voltageMax = 4.2f) :
|
||||
batteryVoltageMin(voltageMin),
|
||||
batteryVoltageMax(voltageMax)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @param milliVolt
|
||||
* @return a value in the rage of [0, 100] which represents [0%, 100%] charge
|
||||
*/
|
||||
uint8_t estimateCharge(uint32_t milliVolt) const;
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
#include "EstimatedPower.h"
|
||||
|
||||
bool EstimatedPower::supportsMetric(MetricType type) const {
|
||||
switch (type) {
|
||||
using enum MetricType;
|
||||
case BatteryVoltage:
|
||||
case ChargeLevel:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool EstimatedPower::getMetric(MetricType type, MetricData& data) {
|
||||
switch (type) {
|
||||
using enum MetricType;
|
||||
case BatteryVoltage:
|
||||
return chargeFromAdcVoltage->readBatteryVoltageSampled(data.valueAsUint32);
|
||||
case ChargeLevel:
|
||||
if (chargeFromAdcVoltage->readBatteryVoltageSampled(data.valueAsUint32)) {
|
||||
data.valueAsUint32 = chargeFromAdcVoltage->estimateChargeLevelFromVoltage(data.valueAsUint32);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <ChargeFromAdcVoltage.h>
|
||||
#include <Tactility/hal/power/PowerDevice.h>
|
||||
|
||||
using tt::hal::power::PowerDevice;
|
||||
|
||||
/**
|
||||
* Uses Voltage measurements to estimate charge.
|
||||
* Supports voltage and charge level metrics.
|
||||
* Can be overridden to further extend supported metrics.
|
||||
*/
|
||||
class EstimatedPower final : public PowerDevice {
|
||||
|
||||
std::unique_ptr<ChargeFromAdcVoltage> chargeFromAdcVoltage;
|
||||
|
||||
public:
|
||||
|
||||
explicit EstimatedPower(ChargeFromAdcVoltage::Configuration configuration) :
|
||||
chargeFromAdcVoltage(std::make_unique<ChargeFromAdcVoltage>(std::move(configuration))) {}
|
||||
|
||||
std::string getName() const override { return "ADC Power Measurement"; }
|
||||
std::string getDescription() const override { return "Power measurement interface via ADC pin"; }
|
||||
|
||||
bool supportsMetric(MetricType type) const override;
|
||||
bool getMetric(MetricType type, MetricData& data) override;
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
#include "St7305Display.h"
|
||||
#include "esp_lcd_st7305.h"
|
||||
|
||||
#include <tactility/log.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
|
||||
static const char* TAG = "ST7305";
|
||||
|
||||
bool St7305Display::createIoHandle(esp_lcd_panel_io_handle_t& outHandle) {
|
||||
LOG_I(TAG, "Starting ST7305 SPI panel IO creation");
|
||||
|
||||
const esp_lcd_panel_io_spi_config_t panel_io_config = {
|
||||
.cs_gpio_num = configuration->csPin,
|
||||
.dc_gpio_num = configuration->dcPin,
|
||||
.spi_mode = 0,
|
||||
.pclk_hz = configuration->pixelClockFrequency,
|
||||
.trans_queue_depth = configuration->transactionQueueDepth,
|
||||
.on_color_trans_done = nullptr,
|
||||
.user_ctx = nullptr,
|
||||
.lcd_cmd_bits = 8,
|
||||
.lcd_param_bits = 8,
|
||||
.flags = {
|
||||
.dc_high_on_cmd = 0,
|
||||
.dc_low_on_data = 0,
|
||||
.dc_low_on_param = 0,
|
||||
.octal_mode = 0,
|
||||
.quad_mode = 0,
|
||||
.sio_mode = 0,
|
||||
.lsb_first = 0,
|
||||
.cs_high_active = 0
|
||||
}
|
||||
};
|
||||
|
||||
if (esp_lcd_new_panel_io_spi(configuration->spiHostDevice, &panel_io_config, &outHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel SPI IO");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool St7305Display::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) {
|
||||
const esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = configuration->resetPin,
|
||||
.color_space = ESP_LCD_COLOR_SPACE_MONOCHROME,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_BIG,
|
||||
.bits_per_pixel = 1,
|
||||
.flags = {
|
||||
.reset_active_high = false
|
||||
},
|
||||
.vendor_config = nullptr
|
||||
};
|
||||
|
||||
if (esp_lcd_new_panel_st7305(ioHandle, &panel_config, &panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create st7305 panel");
|
||||
return false;
|
||||
}
|
||||
|
||||
// ST7305 needs extra delay after reset before init — prevents freeze on fast boot
|
||||
if (esp_lcd_panel_reset(panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to reset st7305 panel");
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(150));
|
||||
|
||||
if (esp_lcd_panel_init(panelHandle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to init st7305 panel");
|
||||
return false;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
|
||||
if (configuration->invertColor) {
|
||||
if (esp_lcd_panel_invert_color(panelHandle, true) != ESP_OK) {
|
||||
LOG_W(TAG, "Failed to apply initial invertColor");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
lvgl_port_display_cfg_t St7305Display::getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) {
|
||||
return lvgl_port_display_cfg_t {
|
||||
.io_handle = ioHandle,
|
||||
.panel_handle = panelHandle,
|
||||
.control_handle = nullptr,
|
||||
.buffer_size = configuration->bufferSize,
|
||||
.double_buffer = false, // Monochrome displays usually use single buffering to save RAM
|
||||
.trans_size = 0,
|
||||
.hres = configuration->horizontalResolution,
|
||||
.vres = configuration->verticalResolution,
|
||||
.monochrome = true, // Enables esp_lvgl_port monochrome converter
|
||||
.rotation = {
|
||||
.swap_xy = false,
|
||||
.mirror_x = true,
|
||||
.mirror_y = false,
|
||||
},
|
||||
.color_format = LV_COLOR_FORMAT_RGB565, // Must be RGB565 for monochrome mode to trigger converter
|
||||
.flags = {
|
||||
.buff_dma = false,
|
||||
.buff_spiram = false,
|
||||
.sw_rotate = false,
|
||||
.swap_bytes = false,
|
||||
.full_refresh = true, // We want full refresh to rewrite the converted block format to ST7305
|
||||
.direct_mode = false
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma once
|
||||
|
||||
#include <EspLcdDisplay.h>
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <driver/spi_common.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_types.h>
|
||||
#include <functional>
|
||||
#include <lvgl.h>
|
||||
|
||||
class St7305Display final : public EspLcdDisplay {
|
||||
|
||||
public:
|
||||
|
||||
class Configuration {
|
||||
|
||||
public:
|
||||
|
||||
Configuration(
|
||||
spi_host_device_t spiHostDevice,
|
||||
gpio_num_t csPin,
|
||||
gpio_num_t dcPin,
|
||||
unsigned int horizontalResolution,
|
||||
unsigned int verticalResolution,
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr,
|
||||
bool swapXY = false,
|
||||
bool mirrorX = false,
|
||||
bool mirrorY = false,
|
||||
bool invertColor = false,
|
||||
uint32_t bufferSize = 0
|
||||
) : spiHostDevice(spiHostDevice),
|
||||
csPin(csPin),
|
||||
dcPin(dcPin),
|
||||
horizontalResolution(horizontalResolution),
|
||||
verticalResolution(verticalResolution),
|
||||
swapXY(swapXY),
|
||||
mirrorX(mirrorX),
|
||||
mirrorY(mirrorY),
|
||||
invertColor(invertColor),
|
||||
bufferSize(bufferSize),
|
||||
touch(std::move(touch))
|
||||
{
|
||||
if (this->bufferSize == 0) {
|
||||
// For monochrome display, full pixel count is expected for buffer size
|
||||
this->bufferSize = horizontalResolution * verticalResolution;
|
||||
}
|
||||
}
|
||||
|
||||
spi_host_device_t spiHostDevice;
|
||||
gpio_num_t csPin;
|
||||
gpio_num_t dcPin;
|
||||
gpio_num_t resetPin = GPIO_NUM_NC;
|
||||
unsigned int pixelClockFrequency = 10'000'000; // 10MHz SPI clock for ST7305
|
||||
size_t transactionQueueDepth = 10;
|
||||
unsigned int horizontalResolution;
|
||||
unsigned int verticalResolution;
|
||||
bool swapXY = false;
|
||||
bool mirrorX = false;
|
||||
bool mirrorY = false;
|
||||
bool invertColor = false;
|
||||
uint32_t bufferSize = 0;
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch;
|
||||
std::function<void(uint8_t)> _Nullable backlightDutyFunction = nullptr;
|
||||
};
|
||||
|
||||
private:
|
||||
|
||||
std::unique_ptr<Configuration> configuration;
|
||||
|
||||
bool createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) override;
|
||||
|
||||
bool createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t& panelHandle) override;
|
||||
|
||||
lvgl_port_display_cfg_t getLvglPortDisplayConfig(esp_lcd_panel_io_handle_t ioHandle, esp_lcd_panel_handle_t panelHandle) override;
|
||||
|
||||
public:
|
||||
|
||||
explicit St7305Display(std::unique_ptr<Configuration> inConfiguration) :
|
||||
configuration(std::move(inConfiguration))
|
||||
{}
|
||||
|
||||
std::string getName() const override { return "ST7305"; }
|
||||
|
||||
std::string getDescription() const override { return "ST7305 monochrome reflective LCD display"; }
|
||||
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> _Nullable getTouchDevice() override { return configuration->touch; }
|
||||
|
||||
void setBacklightDuty(uint8_t backlightDuty) override {
|
||||
if (configuration->backlightDutyFunction != nullptr) {
|
||||
configuration->backlightDutyFunction(backlightDuty);
|
||||
}
|
||||
}
|
||||
|
||||
bool supportsBacklightDuty() const override { return configuration->backlightDutyFunction != nullptr; }
|
||||
|
||||
};
|
||||
|
||||
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
|
||||
@@ -0,0 +1,300 @@
|
||||
#include "esp_lcd_st7305.h"
|
||||
#include "soc/soc_caps.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_lcd_types.h"
|
||||
#include <stdlib.h>
|
||||
#include <sys/cdefs.h>
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "esp_lcd_panel_interface.h"
|
||||
#include "esp_lcd_panel_io.h"
|
||||
#include "esp_lcd_panel_vendor.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
#include "esp_lcd_panel_commands.h"
|
||||
#include "driver/gpio.h"
|
||||
#include <string.h>
|
||||
#include "esp_log.h"
|
||||
|
||||
static const char *TAG = "st7305";
|
||||
|
||||
static esp_err_t panel_st7305_del(esp_lcd_panel_t *panel);
|
||||
static esp_err_t panel_st7305_reset(esp_lcd_panel_t *panel);
|
||||
static esp_err_t panel_st7305_init(esp_lcd_panel_t *panel);
|
||||
static esp_err_t panel_st7305_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data);
|
||||
static esp_err_t panel_st7305_invert_color(esp_lcd_panel_t *panel, bool invert_color_data);
|
||||
static esp_err_t panel_st7305_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y);
|
||||
static esp_err_t panel_st7305_swap_xy(esp_lcd_panel_t *panel, bool swap_axes);
|
||||
static esp_err_t panel_st7305_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap);
|
||||
static esp_err_t panel_st7305_disp_on_off(esp_lcd_panel_t *panel, bool off);
|
||||
|
||||
typedef struct {
|
||||
esp_lcd_panel_t base;
|
||||
esp_lcd_panel_io_handle_t io;
|
||||
int reset_gpio_num;
|
||||
bool reset_level;
|
||||
int x_gap;
|
||||
int y_gap;
|
||||
int width;
|
||||
int height;
|
||||
uint8_t rotation;
|
||||
uint8_t madctl_val;
|
||||
const st7305_lcd_init_cmd_t *init_cmds;
|
||||
uint16_t init_cmds_size;
|
||||
uint8_t *draw_buffer;
|
||||
} st7305_panel_t;
|
||||
|
||||
static const st7305_lcd_init_cmd_t st7305_init_cmds[] = {
|
||||
{0xD6, (uint8_t[]){0x17, 0x02}, 2, 0}, // NVM Load Control
|
||||
{0xD1, (uint8_t[]){0x01}, 1, 0}, // Booster Enable
|
||||
{0xC0, (uint8_t[]){0x11, 0x04}, 2, 0}, // Gate Voltage Setting
|
||||
{0xC1, (uint8_t[]){0x41, 0x41, 0x41, 0x41}, 4, 0}, // VSHP Setting
|
||||
{0xC2, (uint8_t[]){0x19, 0x19, 0x19, 0x19}, 4, 0}, // VSLP Setting
|
||||
{0xC4, (uint8_t[]){0x41, 0x41, 0x41, 0x41}, 4, 0}, // VSHN Setting
|
||||
{0xC5, (uint8_t[]){0x19, 0x13, 0x19, 0x19}, 4, 0}, // VSLN Setting
|
||||
{0xD8, (uint8_t[]){0xA6, 0xE9}, 2, 0}, // OSC Setting
|
||||
{0xB2, (uint8_t[]){0x05}, 1, 0}, // Frame Rate Control
|
||||
{0xB3, (uint8_t[]){0xE5, 0xF6, 0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45}, 10, 0}, // Gate EQ HPM
|
||||
{0xB4, (uint8_t[]){0x05, 0x46, 0x77, 0x77, 0x77, 0x77, 0x76, 0x45}, 8, 0}, // Gate EQ LPM
|
||||
{0x62, (uint8_t[]){0x32, 0x03, 0x1F}, 3, 0}, // Gate Timing Control
|
||||
{0xB7, (uint8_t[]){0x13}, 1, 0}, // Source EQ Enable
|
||||
{0xB0, (uint8_t[]){0x64}, 1, 0}, // Gate Line Setting: 300 line (0x64 = 100 * 3)
|
||||
{0x11, NULL, 0, 200}, // Sleep out
|
||||
{0xC9, (uint8_t[]){0x00}, 1, 0}, // Source Voltage Select
|
||||
{0x36, (uint8_t[]){0x48}, 1, 0}, // Memory Data Access Control (MX=1, DO=1)
|
||||
{0x3A, (uint8_t[]){0x11}, 1, 0}, // Data Format Select: 1bpp
|
||||
{0xB9, (uint8_t[]){0x20}, 1, 0}, // Gamma Mode Setting
|
||||
{0xB8, (uint8_t[]){0x29}, 1, 0}, // Panel Setting
|
||||
{0x21, NULL, 0, 0}, // Display Inversion On
|
||||
{0x2A, (uint8_t[]){0x12, 0x2A}, 2, 0}, // Column Address Setting
|
||||
{0x2B, (uint8_t[]){0x00, 0xC7}, 2, 0}, // Row Address Setting
|
||||
{0x35, (uint8_t[]){0x00}, 1, 0}, // TE Line
|
||||
{0xD0, (uint8_t[]){0xFF}, 1, 0}, // Auto power down ON
|
||||
{0x38, NULL, 0, 0}, // High Power Mode ON
|
||||
{0x29, NULL, 0, 100}, // Display ON
|
||||
};
|
||||
|
||||
esp_err_t esp_lcd_new_panel_st7305(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config,
|
||||
esp_lcd_panel_handle_t *ret_panel)
|
||||
{
|
||||
esp_err_t ret = ESP_OK;
|
||||
st7305_panel_t *st7305 = NULL;
|
||||
gpio_config_t io_conf = { 0 };
|
||||
|
||||
ESP_GOTO_ON_FALSE(io && panel_dev_config && ret_panel, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
|
||||
st7305 = (st7305_panel_t *)calloc(1, sizeof(st7305_panel_t));
|
||||
ESP_GOTO_ON_FALSE(st7305, ESP_ERR_NO_MEM, err, TAG, "no mem for st7305 panel");
|
||||
|
||||
if (panel_dev_config->reset_gpio_num >= 0) {
|
||||
io_conf.mode = GPIO_MODE_OUTPUT;
|
||||
io_conf.pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num;
|
||||
ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
|
||||
}
|
||||
st7305->width = ST7305_WIDTH;
|
||||
st7305->height = ST7305_HEIGHT;
|
||||
st7305->madctl_val = 0x48; // MX=1, DO=1
|
||||
st7305->rotation = 0;
|
||||
|
||||
st7305->io = io;
|
||||
st7305->reset_gpio_num = panel_dev_config->reset_gpio_num;
|
||||
st7305->reset_level = panel_dev_config->flags.reset_active_high;
|
||||
if (panel_dev_config->vendor_config) {
|
||||
st7305->init_cmds = ((st7305_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds;
|
||||
st7305->init_cmds_size = ((st7305_vendor_config_t *)panel_dev_config->vendor_config)->init_cmds_size;
|
||||
} else {
|
||||
st7305->init_cmds = st7305_init_cmds;
|
||||
st7305->init_cmds_size = sizeof(st7305_init_cmds) / sizeof(st7305_lcd_init_cmd_t);
|
||||
}
|
||||
|
||||
st7305->draw_buffer = heap_caps_malloc(15000, MALLOC_CAP_DMA);
|
||||
ESP_GOTO_ON_FALSE(st7305->draw_buffer, ESP_ERR_NO_MEM, err, TAG, "no mem for st7305 draw buffer");
|
||||
memset(st7305->draw_buffer, 0, 15000);
|
||||
st7305->base.del = panel_st7305_del;
|
||||
st7305->base.reset = panel_st7305_reset;
|
||||
st7305->base.init = panel_st7305_init;
|
||||
st7305->base.draw_bitmap = panel_st7305_draw_bitmap;
|
||||
st7305->base.invert_color = panel_st7305_invert_color;
|
||||
st7305->base.set_gap = panel_st7305_set_gap;
|
||||
st7305->base.mirror = panel_st7305_mirror;
|
||||
st7305->base.swap_xy = panel_st7305_swap_xy;
|
||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
|
||||
st7305->base.disp_off = panel_st7305_disp_on_off;
|
||||
#else
|
||||
st7305->base.disp_on_off = panel_st7305_disp_on_off;
|
||||
#endif
|
||||
*ret_panel = &(st7305->base);
|
||||
ESP_LOGD(TAG, "new st7305 panel @%p", st7305);
|
||||
|
||||
return ESP_OK;
|
||||
|
||||
err:
|
||||
if (st7305) {
|
||||
if (panel_dev_config->reset_gpio_num >= 0) {
|
||||
gpio_reset_pin(panel_dev_config->reset_gpio_num);
|
||||
}
|
||||
if (st7305->draw_buffer) {
|
||||
free(st7305->draw_buffer);
|
||||
}
|
||||
free(st7305);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_del(esp_lcd_panel_t *panel)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
if (st7305->reset_gpio_num >= 0) {
|
||||
gpio_reset_pin(st7305->reset_gpio_num);
|
||||
}
|
||||
if (st7305->draw_buffer) {
|
||||
free(st7305->draw_buffer);
|
||||
}
|
||||
free(st7305);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_reset(esp_lcd_panel_t *panel)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
if (st7305->reset_gpio_num >= 0) {
|
||||
gpio_set_level(st7305->reset_gpio_num, !st7305->reset_level);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
gpio_set_level(st7305->reset_gpio_num, st7305->reset_level);
|
||||
vTaskDelay(pdMS_TO_TICKS(20));
|
||||
gpio_set_level(st7305->reset_gpio_num, !st7305->reset_level);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_init(esp_lcd_panel_t *panel)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||
|
||||
for (size_t i = 0; i < st7305->init_cmds_size; i++) {
|
||||
if (st7305->init_cmds[i].data_bytes > 0) {
|
||||
esp_lcd_panel_io_tx_param(io, st7305->init_cmds[i].cmd,
|
||||
st7305->init_cmds[i].data,
|
||||
st7305->init_cmds[i].data_bytes);
|
||||
} else {
|
||||
esp_lcd_panel_io_tx_param(io, st7305->init_cmds[i].cmd, NULL, 0);
|
||||
}
|
||||
if (st7305->init_cmds[i].delay_ms > 0) {
|
||||
vTaskDelay(pdMS_TO_TICKS(st7305->init_cmds[i].delay_ms));
|
||||
}
|
||||
}
|
||||
|
||||
// Explicitly clear display RAM to white
|
||||
if (st7305->draw_buffer) {
|
||||
memset(st7305->draw_buffer, 0xFF, 15000); // 0xFF is White
|
||||
uint8_t caset[] = {0x12, 0x2A};
|
||||
uint8_t raset[] = {0x00, 0xC7};
|
||||
esp_lcd_panel_io_tx_param(io, ST7305_CMD_CASET, caset, sizeof(caset));
|
||||
esp_lcd_panel_io_tx_param(io, ST7305_CMD_RASET, raset, sizeof(raset));
|
||||
esp_lcd_panel_io_tx_color(io, ST7305_CMD_RAMWR, st7305->draw_buffer, 15000);
|
||||
vTaskDelay(pdMS_TO_TICKS(50));
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_draw_bitmap(esp_lcd_panel_t *panel, int x_start, int y_start, int x_end, int y_end, const void *color_data)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||
|
||||
const uint8_t *src = (const uint8_t *)color_data;
|
||||
|
||||
// Convert from vertical-page format (SSD1306-style) to ST7305 landscape 2x4 block format
|
||||
for (int y = 0; y < 300; y++) {
|
||||
int inv_y = 299 - y;
|
||||
int block_y = inv_y >> 2;
|
||||
int local_y = inv_y & 3;
|
||||
|
||||
int src_y_div_8 = y >> 3;
|
||||
int src_y_mod_8 = y & 7;
|
||||
uint8_t src_bit_mask = 1 << src_y_mod_8;
|
||||
|
||||
for (int x = 0; x < 400; x++) {
|
||||
int byte_x = x >> 1;
|
||||
int local_x = x & 1;
|
||||
|
||||
int dest_byte_idx = byte_x * 75 + block_y;
|
||||
int dest_bit_pos = 7 - ((local_y << 1) | local_x);
|
||||
|
||||
int src_byte_idx = 400 * src_y_div_8 + x;
|
||||
|
||||
// Read standard vertical page byte bit
|
||||
bool is_pixel_set = (src[src_byte_idx] & src_bit_mask) != 0;
|
||||
|
||||
// Invert the pixel logic if required, standard:
|
||||
// esp_lvgl_port monochrome transform clears the bit (0) for light/chroma colors
|
||||
// and sets the bit (1) for dark/black.
|
||||
// In ST7305 display RAM, White/Light is 1, Black/Dark is 0.
|
||||
// So we write: 1 (White) if is_pixel_set is false (light), and 0 (Black) if is_pixel_set is true (dark).
|
||||
if (!is_pixel_set) {
|
||||
st7305->draw_buffer[dest_byte_idx] |= (1 << dest_bit_pos);
|
||||
} else {
|
||||
st7305->draw_buffer[dest_byte_idx] &= ~(1 << dest_bit_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t caset[] = {0x12, 0x2A};
|
||||
uint8_t raset[] = {0x00, 0xC7};
|
||||
|
||||
esp_lcd_panel_io_tx_param(io, ST7305_CMD_CASET, caset, sizeof(caset));
|
||||
esp_lcd_panel_io_tx_param(io, ST7305_CMD_RASET, raset, sizeof(raset));
|
||||
esp_lcd_panel_io_tx_color(io, ST7305_CMD_RAMWR, st7305->draw_buffer, 15000);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_invert_color(esp_lcd_panel_t *panel, bool invert_color_data)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
return esp_lcd_panel_io_tx_param(st7305->io, invert_color_data ? ST7305_CMD_INVON : ST7305_CMD_INVOFF, NULL, 0);
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_mirror(esp_lcd_panel_t *panel, bool mirror_x, bool mirror_y)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||
if (mirror_x) {
|
||||
st7305->madctl_val |= ST7305_MADCTL_MX;
|
||||
} else {
|
||||
st7305->madctl_val &= ~ST7305_MADCTL_MX;
|
||||
}
|
||||
if (mirror_y) {
|
||||
st7305->madctl_val |= ST7305_MADCTL_MY;
|
||||
} else {
|
||||
st7305->madctl_val &= ~ST7305_MADCTL_MY;
|
||||
}
|
||||
return esp_lcd_panel_io_tx_param(io, ST7305_CMD_MADCTL, &st7305->madctl_val, 1);
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_swap_xy(esp_lcd_panel_t *panel, bool swap_axes)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
esp_lcd_panel_io_handle_t io = st7305->io;
|
||||
if (swap_axes) {
|
||||
st7305->madctl_val |= ST7305_MADCTL_MV;
|
||||
} else {
|
||||
st7305->madctl_val &= ~ST7305_MADCTL_MV;
|
||||
}
|
||||
return esp_lcd_panel_io_tx_param(io, ST7305_CMD_MADCTL, (uint8_t[]) { st7305->madctl_val }, 1);
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_set_gap(esp_lcd_panel_t *panel, int x_gap, int y_gap)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
st7305->x_gap = x_gap;
|
||||
st7305->y_gap = y_gap;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t panel_st7305_disp_on_off(esp_lcd_panel_t *panel, bool off)
|
||||
{
|
||||
st7305_panel_t *st7305 = __containerof(panel, st7305_panel_t, base);
|
||||
return esp_lcd_panel_io_tx_param(st7305->io, off ? ST7305_CMD_DISPOFF : ST7305_CMD_DISPON, NULL, 0);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include "esp_lcd_types.h"
|
||||
#include "esp_lcd_panel_vendor.h"
|
||||
#include "sdkconfig.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
int cmd; /*<! The specific LCD command */
|
||||
const void *data; /*<! Buffer that holds the command specific data */
|
||||
size_t data_bytes; /*<! Size of `data` in memory, in bytes */
|
||||
unsigned int delay_ms; /*<! Delay in milliseconds after this command */
|
||||
} st7305_lcd_init_cmd_t;
|
||||
|
||||
typedef struct {
|
||||
const st7305_lcd_init_cmd_t *init_cmds;
|
||||
uint16_t init_cmds_size;
|
||||
} st7305_vendor_config_t;
|
||||
|
||||
#define ST7305_WIDTH 400
|
||||
#define ST7305_HEIGHT 300
|
||||
|
||||
#define ST7305_CMD_NOP 0x00
|
||||
#define ST7305_CMD_SWRESET 0x01
|
||||
#define ST7305_CMD_SLPOUT 0x11
|
||||
#define ST7305_CMD_NORON 0x13
|
||||
#define ST7305_CMD_INVOFF 0x20
|
||||
#define ST7305_CMD_INVON 0x21
|
||||
#define ST7305_CMD_DISPOFF 0x28
|
||||
#define ST7305_CMD_DISPON 0x29
|
||||
#define ST7305_CMD_CASET 0x2A
|
||||
#define ST7305_CMD_RASET 0x2B
|
||||
#define ST7305_CMD_RAMWR 0x2C
|
||||
#define ST7305_CMD_MADCTL 0x36
|
||||
|
||||
#define ST7305_MADCTL_MY 0x80
|
||||
#define ST7305_MADCTL_MX 0x40
|
||||
#define ST7305_MADCTL_MV 0x20
|
||||
#define ST7305_MADCTL_ML 0x10
|
||||
|
||||
esp_err_t esp_lcd_new_panel_st7305(const esp_lcd_panel_io_handle_t io, const esp_lcd_panel_dev_config_t *panel_dev_config, esp_lcd_panel_handle_t *ret_panel);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,3 +0,0 @@
|
||||
# ST7789
|
||||
|
||||
A basic ESP32 LVGL driver for ST7789 parallel i8080 displays.
|
||||
@@ -1,345 +0,0 @@
|
||||
#include "St7789i8080Display.h"
|
||||
#include <tactility/log.h>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
#include <esp_lvgl_port.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <lvgl.h>
|
||||
|
||||
constexpr auto* TAG = "St7789i8080Display";
|
||||
static St7789i8080Display* g_display_instance = nullptr;
|
||||
|
||||
// ST7789 initialization commands
|
||||
typedef struct {
|
||||
uint8_t cmd;
|
||||
uint8_t data[14];
|
||||
uint8_t len;
|
||||
} lcd_init_cmd_t;
|
||||
|
||||
static const lcd_init_cmd_t st7789_init_cmds[] = {
|
||||
{0x11, {0}, 0 | 0x80},
|
||||
{0x36, {0x08}, 1},
|
||||
{0x3A, {0X05}, 1},
|
||||
{0x20, {0}, 0},
|
||||
{0xB2, {0X0B, 0X0B, 0X00, 0X33, 0X33}, 5},
|
||||
{0xB7, {0X75}, 1},
|
||||
{0xBB, {0X28}, 1},
|
||||
{0xC0, {0X2C}, 1},
|
||||
{0xC2, {0X01}, 1},
|
||||
{0xC3, {0X1F}, 1},
|
||||
{0xC6, {0X13}, 1},
|
||||
{0xD0, {0XA7}, 1},
|
||||
{0xD0, {0XA4, 0XA1}, 2},
|
||||
{0xD6, {0XA1}, 1},
|
||||
{0xE0, {0XF0, 0X05, 0X0A, 0X06, 0X06, 0X03, 0X2B, 0X32, 0X43, 0X36, 0X11, 0X10, 0X2B, 0X32}, 14},
|
||||
{0xE1, {0XF0, 0X08, 0X0C, 0X0B, 0X09, 0X24, 0X2B, 0X22, 0X43, 0X38, 0X15, 0X16, 0X2F, 0X37}, 14},
|
||||
};
|
||||
|
||||
// Callback when color transfer is done
|
||||
static bool notify_lvgl_flush_ready(esp_lcd_panel_io_handle_t panel_io,
|
||||
esp_lcd_panel_io_event_data_t *edata,
|
||||
void *user_ctx) {
|
||||
lv_display_t *disp = (lv_display_t *)user_ctx;
|
||||
lv_display_flush_ready(disp);
|
||||
return false;
|
||||
}
|
||||
|
||||
St7789i8080Display::St7789i8080Display(const Configuration& config)
|
||||
: configuration(config), lock(std::make_shared<std::mutex>()) {
|
||||
|
||||
// Validate configuration
|
||||
if (!configuration.isValid()) {
|
||||
LOG_E(TAG, "Invalid configuration: resolution must be set");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool St7789i8080Display::createI80Bus() {
|
||||
LOG_I(TAG, "Creating I80 bus");
|
||||
|
||||
// Create I80 bus configuration
|
||||
esp_lcd_i80_bus_config_t bus_cfg = {
|
||||
.dc_gpio_num = configuration.dcPin,
|
||||
.wr_gpio_num = configuration.wrPin,
|
||||
.clk_src = LCD_CLK_SRC_DEFAULT,
|
||||
.data_gpio_nums = {
|
||||
configuration.dataPins[0], configuration.dataPins[1],
|
||||
configuration.dataPins[2], configuration.dataPins[3],
|
||||
configuration.dataPins[4], configuration.dataPins[5],
|
||||
configuration.dataPins[6], configuration.dataPins[7],
|
||||
},
|
||||
.bus_width = configuration.busWidth,
|
||||
.max_transfer_bytes = configuration.bufferSize * sizeof(uint16_t),
|
||||
.psram_trans_align = 64,
|
||||
.sram_trans_align = 4
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_lcd_new_i80_bus(&bus_cfg, &i80BusHandle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create I80 bus: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool St7789i8080Display::createPanelIO() {
|
||||
LOG_I(TAG, "Creating panel IO");
|
||||
|
||||
// Create panel IO with proper callback
|
||||
esp_lcd_panel_io_i80_config_t io_cfg = {
|
||||
.cs_gpio_num = configuration.csPin,
|
||||
.pclk_hz = configuration.pixelClockFrequency,
|
||||
.trans_queue_depth = configuration.transactionQueueDepth,
|
||||
.on_color_trans_done = notify_lvgl_flush_ready, // Use proper callback
|
||||
.user_ctx = nullptr, // Will be set when LVGL display is created
|
||||
.lcd_cmd_bits = configuration.lcdCmdBits,
|
||||
.lcd_param_bits = configuration.lcdParamBits,
|
||||
.dc_levels = {
|
||||
.dc_idle_level = configuration.dcLevels.dcIdleLevel,
|
||||
.dc_cmd_level = configuration.dcLevels.dcCmdLevel,
|
||||
.dc_dummy_level = configuration.dcLevels.dcDummyLevel,
|
||||
.dc_data_level = configuration.dcLevels.dcDataLevel,
|
||||
},
|
||||
.flags = {
|
||||
.cs_active_high = configuration.flags.csActiveHigh,
|
||||
.reverse_color_bits = configuration.flags.reverseColorBits,
|
||||
.swap_color_bytes = configuration.flags.swapColorBytes,
|
||||
.pclk_active_neg = configuration.flags.pclkActiveNeg,
|
||||
.pclk_idle_low = configuration.flags.pclkIdleLow
|
||||
}
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_lcd_new_panel_io_i80(i80BusHandle, &io_cfg, &ioHandle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool St7789i8080Display::createPanel() {
|
||||
LOG_I(TAG, "Configuring panel");
|
||||
|
||||
// Create ST7789 panel
|
||||
esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = configuration.resetPin,
|
||||
.rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
|
||||
.bits_per_pixel = 16,
|
||||
.flags = {
|
||||
.reset_active_high = false
|
||||
},
|
||||
.vendor_config = nullptr
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_lcd_new_panel_st7789(ioHandle, &panel_config, &panelHandle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create ST7789 panel: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reset panel
|
||||
ret = esp_lcd_panel_reset(panelHandle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to reset panel: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Initialize panel
|
||||
ret = esp_lcd_panel_init(panelHandle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to init panel: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set gap
|
||||
ret = esp_lcd_panel_set_gap(panelHandle, configuration.gapX, configuration.gapY);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel gap: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set inversion
|
||||
ret = esp_lcd_panel_invert_color(panelHandle, configuration.invertColor);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel inversion: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set mirror
|
||||
ret = esp_lcd_panel_mirror(panelHandle, configuration.mirrorX, configuration.mirrorY);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to set panel mirror: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Turn on display
|
||||
ret = esp_lcd_panel_disp_on_off(panelHandle, true);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to turn display on: %s", esp_err_to_name(ret));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void St7789i8080Display::sendInitCommands() {
|
||||
LOG_I(TAG, "Sending ST7789 init commands");
|
||||
for (const auto& cmd : st7789_init_cmds) {
|
||||
esp_lcd_panel_io_tx_param(ioHandle, cmd.cmd, cmd.data, cmd.len & 0x7F);
|
||||
if (cmd.len & 0x80) {
|
||||
vTaskDelay(pdMS_TO_TICKS(120));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool St7789i8080Display::start() {
|
||||
LOG_I(TAG, "Initializing I8080 ST7789 Display hardware...");
|
||||
|
||||
// Configure RD pin if needed
|
||||
if (configuration.rdPin != GPIO_NUM_NC) {
|
||||
gpio_config_t rd_gpio_config = {
|
||||
.pin_bit_mask = (1ULL << static_cast<uint32_t>(configuration.rdPin)),
|
||||
.mode = GPIO_MODE_OUTPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_DISABLE,
|
||||
};
|
||||
gpio_config(&rd_gpio_config);
|
||||
gpio_set_level(configuration.rdPin, 1);
|
||||
}
|
||||
|
||||
// Calculate buffer size if needed
|
||||
configuration.calculateBufferSize();
|
||||
|
||||
// Allocate buffer based on resolution
|
||||
size_t buffer_size = configuration.bufferSize * LV_COLOR_FORMAT_GET_SIZE(LV_COLOR_FORMAT_RGB565);
|
||||
buf1 = (uint8_t*)heap_caps_malloc(buffer_size, MALLOC_CAP_DMA);
|
||||
if (!buf1) {
|
||||
LOG_E(TAG, "Failed to allocate display buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create I80 bus
|
||||
if (!createI80Bus()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create panel IO
|
||||
if (!createPanelIO()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create panel
|
||||
if (!createPanel()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_I(TAG, "Display hardware initialized");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool St7789i8080Display::stop() {
|
||||
// Turn off display
|
||||
if (panelHandle) {
|
||||
esp_lcd_panel_disp_on_off(panelHandle, false);
|
||||
}
|
||||
|
||||
// Destroy in reverse order: panel, IO, bus
|
||||
if (panelHandle) {
|
||||
esp_lcd_panel_del(panelHandle);
|
||||
panelHandle = nullptr;
|
||||
}
|
||||
if (ioHandle) {
|
||||
esp_lcd_panel_io_del(ioHandle);
|
||||
ioHandle = nullptr;
|
||||
}
|
||||
if (i80BusHandle) {
|
||||
esp_lcd_del_i80_bus(i80BusHandle);
|
||||
i80BusHandle = nullptr;
|
||||
}
|
||||
|
||||
// Free buffer
|
||||
if (buf1) {
|
||||
heap_caps_free(buf1);
|
||||
buf1 = nullptr;
|
||||
}
|
||||
|
||||
// Turn off backlight
|
||||
if (configuration.backlightDutyFunction) {
|
||||
configuration.backlightDutyFunction(0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool St7789i8080Display::startLvgl() {
|
||||
LOG_I(TAG, "Initializing LVGL for ST7789 display");
|
||||
|
||||
// Don't reinitialize hardware if it's already done
|
||||
if (!ioHandle) {
|
||||
LOG_I(TAG, "Hardware not initialized, calling start()");
|
||||
if (!start()) {
|
||||
LOG_E(TAG, "Hardware initialization failed");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_I(TAG, "Hardware already initialized, skipping");
|
||||
}
|
||||
|
||||
// Create LVGL display using lvgl_port
|
||||
lvgl_port_display_cfg_t display_cfg = {
|
||||
.io_handle = ioHandle,
|
||||
.panel_handle = panelHandle,
|
||||
.control_handle = nullptr,
|
||||
.buffer_size = configuration.bufferSize,
|
||||
.double_buffer = false,
|
||||
.trans_size = 0,
|
||||
.hres = configuration.horizontalResolution,
|
||||
.vres = configuration.verticalResolution,
|
||||
.monochrome = false,
|
||||
.rotation = {
|
||||
.swap_xy = configuration.swapXY,
|
||||
.mirror_x = configuration.mirrorX,
|
||||
.mirror_y = configuration.mirrorY,
|
||||
},
|
||||
.color_format = LV_COLOR_FORMAT_RGB565,
|
||||
.flags = {
|
||||
.buff_dma = true,
|
||||
.buff_spiram = false,
|
||||
.sw_rotate = true,
|
||||
.swap_bytes = true,
|
||||
.full_refresh = false,
|
||||
.direct_mode = false
|
||||
}
|
||||
};
|
||||
|
||||
// Create the LVGL display
|
||||
lvglDisplay = lvgl_port_add_disp(&display_cfg);
|
||||
if (!lvglDisplay) {
|
||||
LOG_E(TAG, "Failed to create LVGL display");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Register the callback for color transfer completion
|
||||
esp_lcd_panel_io_callbacks_t cbs = {
|
||||
.on_color_trans_done = notify_lvgl_flush_ready,
|
||||
};
|
||||
esp_lcd_panel_io_register_event_callbacks(ioHandle, &cbs, lvglDisplay);
|
||||
|
||||
g_display_instance = this;
|
||||
LOG_I(TAG, "LVGL display created successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool St7789i8080Display::stopLvgl() {
|
||||
if (lvglDisplay) {
|
||||
lvgl_port_remove_disp(lvglDisplay);
|
||||
lvglDisplay = nullptr;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Tactility/hal/display/DisplayDevice.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_types.h>
|
||||
#include <esp_lcd_panel_st7789.h>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
|
||||
class St7789i8080Display : public tt::hal::display::DisplayDevice {
|
||||
public:
|
||||
struct Configuration {
|
||||
// Pin configuration
|
||||
gpio_num_t csPin;
|
||||
gpio_num_t dcPin;
|
||||
gpio_num_t wrPin;
|
||||
gpio_num_t rdPin;
|
||||
std::array<gpio_num_t, 8> dataPins;
|
||||
gpio_num_t resetPin;
|
||||
gpio_num_t backlightPin;
|
||||
|
||||
// Display resolution configuration
|
||||
uint16_t horizontalResolution;
|
||||
uint16_t verticalResolution;
|
||||
|
||||
// Bus configuration
|
||||
unsigned int pixelClockFrequency = 16 * 1000 * 1000; // 16MHz default
|
||||
size_t busWidth = 8; // 8-bit bus
|
||||
size_t transactionQueueDepth = 40;
|
||||
size_t bufferSize = 0; // Will be calculated if 0
|
||||
|
||||
// LCD command/parameter configuration
|
||||
int lcdCmdBits = 8;
|
||||
int lcdParamBits = 8;
|
||||
|
||||
// DC line level configuration
|
||||
struct {
|
||||
bool dcIdleLevel = 0;
|
||||
bool dcCmdLevel = 0;
|
||||
bool dcDummyLevel = 0;
|
||||
bool dcDataLevel = 1;
|
||||
} dcLevels;
|
||||
|
||||
// Bus flags
|
||||
struct {
|
||||
bool csActiveHigh = false;
|
||||
bool reverseColorBits = false;
|
||||
bool swapColorBytes = true;
|
||||
bool pclkActiveNeg = false;
|
||||
bool pclkIdleLow = false;
|
||||
} flags;
|
||||
|
||||
// Display configuration
|
||||
int gapX = 0;
|
||||
int gapY = 0;
|
||||
bool swapXY = false;
|
||||
bool mirrorX = false;
|
||||
bool mirrorY = false;
|
||||
bool invertColor = true;
|
||||
|
||||
// Additional features
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> touch = nullptr;
|
||||
std::function<void(uint8_t)> backlightDutyFunction = nullptr;
|
||||
|
||||
// Basic constructor - requires resolution to be set separately
|
||||
Configuration(gpio_num_t cs, gpio_num_t dc, gpio_num_t wr, gpio_num_t rd,
|
||||
std::array<gpio_num_t, 8> data, gpio_num_t rst, gpio_num_t bl)
|
||||
: csPin(cs), dcPin(dc), wrPin(wr), rdPin(rd),
|
||||
dataPins(data), resetPin(rst), backlightPin(bl),
|
||||
horizontalResolution(0), verticalResolution(0) {} // Initialize to 0
|
||||
|
||||
// Method to calculate buffer size after resolution is set
|
||||
void calculateBufferSize() {
|
||||
if (bufferSize == 0 && horizontalResolution > 0 && verticalResolution > 0) {
|
||||
bufferSize = horizontalResolution * verticalResolution / 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Validation method
|
||||
bool isValid() const {
|
||||
return horizontalResolution > 0 && verticalResolution > 0;
|
||||
}
|
||||
};
|
||||
|
||||
private:
|
||||
Configuration configuration;
|
||||
esp_lcd_i80_bus_handle_t i80BusHandle = nullptr;
|
||||
esp_lcd_panel_io_handle_t ioHandle = nullptr;
|
||||
esp_lcd_panel_handle_t panelHandle = nullptr;
|
||||
lv_display_t* lvglDisplay = nullptr;
|
||||
std::shared_ptr<std::mutex> lock;
|
||||
uint8_t* buf1 = nullptr;
|
||||
|
||||
// Internal initialization methods
|
||||
void sendInitCommands();
|
||||
bool createI80Bus();
|
||||
bool createPanelIO();
|
||||
bool createPanel();
|
||||
|
||||
public:
|
||||
explicit St7789i8080Display(const Configuration& config);
|
||||
lv_display_t* getLvglDisplay() const override { return lvglDisplay; }
|
||||
std::string getName() const override { return "I8080 ST7789"; }
|
||||
std::string getDescription() const override { return "I8080-based ST7789 display"; }
|
||||
|
||||
// Lifecycle
|
||||
bool start() override;
|
||||
bool stop() override;
|
||||
bool startLvgl() override;
|
||||
bool stopLvgl() override;
|
||||
|
||||
// Capabilities
|
||||
bool supportsLvgl() const override { return true; }
|
||||
bool supportsDisplayDriver() const override { return false; }
|
||||
bool supportsBacklightDuty() const override { return configuration.backlightDutyFunction != nullptr; }
|
||||
|
||||
// Touch and backlight
|
||||
std::shared_ptr<tt::hal::touch::TouchDevice> getTouchDevice() override { return configuration.touch; }
|
||||
std::shared_ptr<tt::hal::display::DisplayDriver> getDisplayDriver() override { return nullptr; }
|
||||
|
||||
void setBacklightDuty(uint8_t backlightDuty) override {
|
||||
if (configuration.backlightDutyFunction != nullptr) {
|
||||
configuration.backlightDutyFunction(backlightDuty);
|
||||
}
|
||||
}
|
||||
|
||||
// Hardware access methods
|
||||
esp_lcd_panel_io_handle_t getIoHandle() const { return ioHandle; }
|
||||
esp_lcd_panel_handle_t getPanelHandle() const { return panelHandle; }
|
||||
|
||||
// Resolution access methods
|
||||
uint16_t getHorizontalResolution() const { return configuration.horizontalResolution; }
|
||||
uint16_t getVerticalResolution() const { return configuration.verticalResolution; }
|
||||
};
|
||||
|
||||
// Factory function for registration
|
||||
std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
|
||||
@@ -4,8 +4,8 @@ include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(axs15231b-module
|
||||
tactility_add_module(axp192-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel platform-esp32 esp_lcd_axs15231b driver
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
# AXP192 power management IC
|
||||
|
||||
A driver for the X-Powers `AXP192` PMIC: DCDC1-3/LDO2-3/EXTEN rail enable and voltage
|
||||
control, battery voltage/current/power readback, charge enable/status, and system power off.
|
||||
Registers as a `power-supply` child device (voltage, current, is-charging, charge control,
|
||||
power off) alongside its own public API for full rail control.
|
||||
|
||||
Also includes `axp192-backlight`: a `BACKLIGHT_TYPE` driver for a backlight wired to one of
|
||||
the AXP192's rails, declared as a devicetree child node of the `axp192` device it dims. Its
|
||||
`rail`, `min-millivolt` and `max-millivolt` properties select the channel and map the 0-255
|
||||
brightness level onto that rail's voltage range; brightness level 0 disables the rail outright.
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,24 @@
|
||||
description: >
|
||||
Backlight driven by a switchable/adjustable AXP192 power rail. Must be declared as a
|
||||
child node of the axp192 device it dims. Maps the 0-255 brightness level onto the
|
||||
rail's [min-millivolt,max-millivolt] voltage range; level 0 disables the rail outright.
|
||||
|
||||
compatible: "axp192-backlight"
|
||||
|
||||
properties:
|
||||
rail:
|
||||
type: int
|
||||
required: true
|
||||
description: The Axp192Rail powering the backlight (e.g. AXP192_RAIL_LDO2)
|
||||
min-millivolt:
|
||||
type: int
|
||||
default: 0
|
||||
description: Rail voltage at brightness level 0 (the rail is disabled rather than actually set to this value)
|
||||
max-millivolt:
|
||||
type: int
|
||||
required: true
|
||||
description: Rail voltage at the maximum brightness level (255)
|
||||
brightness-default:
|
||||
type: int
|
||||
default: 255
|
||||
description: Default brightness level, applied by set_brightness_default()
|
||||
@@ -0,0 +1,5 @@
|
||||
description: X-Powers AXP192 power management IC
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "x-powers,axp192"
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module axs15231b_module;
|
||||
extern struct Module axp192_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axp192.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(axp192, struct Axp192Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axp192_backlight.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(axp192_backlight, struct Axp192BacklightConfig)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Axp192Config {
|
||||
/** Address on bus */
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
/** Switchable/adjustable power rails of the AXP192. */
|
||||
enum Axp192Rail {
|
||||
AXP192_RAIL_DCDC1,
|
||||
AXP192_RAIL_DCDC2,
|
||||
AXP192_RAIL_DCDC3,
|
||||
AXP192_RAIL_LDO2,
|
||||
AXP192_RAIL_LDO3,
|
||||
/** EXTEN output switch; does not support voltage control. */
|
||||
AXP192_RAIL_EXTEN,
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Checks whether a power rail is currently enabled.
|
||||
*/
|
||||
error_t axp192_is_rail_enabled(struct Device* device, enum Axp192Rail rail, bool* enabled);
|
||||
|
||||
/**
|
||||
* @brief Enables or disables a power rail.
|
||||
*/
|
||||
error_t axp192_set_rail_enabled(struct Device* device, enum Axp192Rail rail, bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Sets the output voltage of a power rail.
|
||||
* @retval ERROR_NOT_SUPPORTED for AXP192_RAIL_EXTEN, which has no voltage control
|
||||
* @retval ERROR_INVALID_ARGUMENT when millivolts is outside the rail's supported range
|
||||
* (DCDC1/DCDC3: 700-3500mV, DCDC2: 700-2275mV, LDO2/LDO3: 1800-3300mV)
|
||||
*/
|
||||
error_t axp192_set_rail_voltage(struct Device* device, enum Axp192Rail rail, uint16_t millivolts);
|
||||
|
||||
/** Battery voltage in millivolts. */
|
||||
error_t axp192_get_battery_voltage(struct Device* device, uint16_t* millivolts);
|
||||
|
||||
/** Battery charge current in milliamps (0 when not charging). */
|
||||
error_t axp192_get_battery_charge_current(struct Device* device, uint16_t* milliamps);
|
||||
|
||||
/** Battery discharge current in milliamps (0 when not discharging). */
|
||||
error_t axp192_get_battery_discharge_current(struct Device* device, uint16_t* milliamps);
|
||||
|
||||
/** Battery power in microwatts. */
|
||||
error_t axp192_get_battery_power(struct Device* device, uint32_t* microwatts);
|
||||
|
||||
/** Whether the battery is currently charging. */
|
||||
error_t axp192_is_charging(struct Device* device, bool* charging);
|
||||
|
||||
/** Whether the charger is allowed to charge the battery. */
|
||||
error_t axp192_is_charge_enabled(struct Device* device, bool* enabled);
|
||||
|
||||
/** Enables or disables battery charging. */
|
||||
error_t axp192_set_charge_enabled(struct Device* device, bool enabled);
|
||||
|
||||
/** Powers off the system (does not return on success). */
|
||||
error_t axp192_power_off(struct Device* device);
|
||||
|
||||
/** Configures GPIO1 as the PWM1 output (rather than GPIO/ADC input/output). */
|
||||
error_t axp192_set_gpio1_pwm1_output(struct Device* device);
|
||||
|
||||
/** Sets the PWM1 duty cycle (0 = always low, 255 = always high). */
|
||||
error_t axp192_set_pwm1_duty_cycle(struct Device* device, uint8_t duty);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <drivers/axp192.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Devicetree configuration for a backlight driven by a switchable/adjustable AXP192 power rail.
|
||||
*/
|
||||
struct Axp192BacklightConfig {
|
||||
/** The AXP192 rail powering the backlight */
|
||||
enum Axp192Rail rail;
|
||||
/** Rail voltage at brightness level 0. Brightness level 0 always disables the rail outright,
|
||||
* rather than actually driving it to this voltage - see set_brightness() in BacklightApi. */
|
||||
uint16_t min_millivolt;
|
||||
/** Rail voltage at the maximum brightness level (255) */
|
||||
uint16_t max_millivolt;
|
||||
/** Default brightness level, applied by set_brightness_default() */
|
||||
uint8_t brightness_default;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,445 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axp192.h>
|
||||
#include <axp192_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Axp192Config*>((device)->config))
|
||||
|
||||
/** Reference: https://github.com/tuupola/axp192 (register map and ADC/voltage formulas) */
|
||||
static constexpr uint8_t REG_MODE_CHGSTATUS = 0x01U; // bit6: battery is charging
|
||||
static constexpr uint8_t REG_EXTEN_DCDC2_CONTROL = 0x10U; // bit2: EXTEN, bit0: DCDC2
|
||||
static constexpr uint8_t REG_DCDC13_LDO23_CONTROL = 0x12U; // bit3: LDO3, bit2: LDO2, bit1: DCDC3, bit0: DCDC1
|
||||
static constexpr uint8_t REG_DCDC2_VOLTAGE = 0x23U;
|
||||
static constexpr uint8_t REG_DCDC1_VOLTAGE = 0x26U;
|
||||
static constexpr uint8_t REG_DCDC3_VOLTAGE = 0x27U;
|
||||
static constexpr uint8_t REG_LDO23_VOLTAGE = 0x28U; // bits7-4: LDO2, bits3-0: LDO3
|
||||
static constexpr uint8_t REG_SHUTDOWN_BATTERY_CHGLED_CONTROL = 0x32U; // bit7: power off
|
||||
static constexpr uint8_t REG_CHARGE_CONTROL_1 = 0x33U; // bit7: charging enabled
|
||||
static constexpr uint8_t REG_BATTERY_POWER = 0x70U; // 3 bytes, 0.55uW/LSB
|
||||
static constexpr uint8_t REG_BATTERY_VOLTAGE = 0x78U; // 2 bytes, 1.1mV/LSB
|
||||
static constexpr uint8_t REG_CHARGE_CURRENT = 0x7AU; // 2 bytes, 0.5mA/LSB
|
||||
static constexpr uint8_t REG_DISCHARGE_CURRENT = 0x7CU; // 2 bytes, 0.5mA/LSB
|
||||
static constexpr uint8_t REG_GPIO1_CONTROL = 0x92U; // function select (0x02 = PWM1 output)
|
||||
static constexpr uint8_t REG_PWM1_DUTY_CYCLE_2 = 0x9AU; // PWM1 duty cycle, low byte
|
||||
|
||||
static constexpr uint8_t GPIO1_FUNCTION_PWM1 = 0x02U;
|
||||
|
||||
static constexpr uint8_t BIT_CHARGING = 1U << 6U;
|
||||
static constexpr uint8_t BIT_CHARGE_ENABLED = 1U << 7U;
|
||||
static constexpr uint8_t BIT_POWER_OFF = 1U << 7U;
|
||||
|
||||
static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50);
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Module axp192_module;
|
||||
|
||||
// region Register helpers
|
||||
|
||||
/** Reads a 2-byte ADC register in AXP192's packed 12-bit format: raw = (high << 4) + low. */
|
||||
static error_t read_adc_raw(Device* device, uint8_t reg, uint16_t* raw) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t buffer[2];
|
||||
error_t err = i2c_controller_read_register(parent, address, reg, buffer, sizeof(buffer), TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*raw = static_cast<uint16_t>((buffer[0] << 4U) + (buffer[1] & 0x0FU));
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t get_rail_enable_bit(Axp192Rail rail, uint8_t* reg, uint8_t* bit) {
|
||||
switch (rail) {
|
||||
case AXP192_RAIL_DCDC1:
|
||||
*reg = REG_DCDC13_LDO23_CONTROL;
|
||||
*bit = 1U << 0U;
|
||||
return ERROR_NONE;
|
||||
case AXP192_RAIL_DCDC2:
|
||||
*reg = REG_EXTEN_DCDC2_CONTROL;
|
||||
*bit = 1U << 0U;
|
||||
return ERROR_NONE;
|
||||
case AXP192_RAIL_DCDC3:
|
||||
*reg = REG_DCDC13_LDO23_CONTROL;
|
||||
*bit = 1U << 1U;
|
||||
return ERROR_NONE;
|
||||
case AXP192_RAIL_LDO2:
|
||||
*reg = REG_DCDC13_LDO23_CONTROL;
|
||||
*bit = 1U << 2U;
|
||||
return ERROR_NONE;
|
||||
case AXP192_RAIL_LDO3:
|
||||
*reg = REG_DCDC13_LDO23_CONTROL;
|
||||
*bit = 1U << 3U;
|
||||
return ERROR_NONE;
|
||||
case AXP192_RAIL_EXTEN:
|
||||
*reg = REG_EXTEN_DCDC2_CONTROL;
|
||||
*bit = 1U << 2U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
error_t axp192_is_rail_enabled(Device* device, Axp192Rail rail, bool* enabled) {
|
||||
uint8_t reg, bit;
|
||||
error_t err = get_rail_enable_bit(rail, ®, &bit);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
err = i2c_controller_register8_get(parent, address, reg, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
*enabled = (value & bit) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp192_set_rail_enabled(Device* device, Axp192Rail rail, bool enabled) {
|
||||
uint8_t reg, bit;
|
||||
error_t err = get_rail_enable_bit(rail, ®, &bit);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
if (enabled) {
|
||||
return i2c_controller_register8_set_bits(parent, address, reg, bit, TIMEOUT);
|
||||
} else {
|
||||
return i2c_controller_register8_reset_bits(parent, address, reg, bit, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
error_t axp192_set_rail_voltage(Device* device, Axp192Rail rail, uint16_t millivolts) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
|
||||
switch (rail) {
|
||||
case AXP192_RAIL_DCDC1:
|
||||
case AXP192_RAIL_DCDC3: {
|
||||
if (millivolts < 700U || millivolts > 3500U) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
uint8_t reg = (rail == AXP192_RAIL_DCDC1) ? REG_DCDC1_VOLTAGE : REG_DCDC3_VOLTAGE;
|
||||
uint8_t value = static_cast<uint8_t>((millivolts - 700U) / 25U);
|
||||
return i2c_controller_register8_set(parent, address, reg, value, TIMEOUT);
|
||||
}
|
||||
case AXP192_RAIL_DCDC2: {
|
||||
if (millivolts < 700U || millivolts > 2275U) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
uint8_t value = static_cast<uint8_t>((millivolts - 700U) / 25U);
|
||||
return i2c_controller_register8_set(parent, address, REG_DCDC2_VOLTAGE, value, TIMEOUT);
|
||||
}
|
||||
case AXP192_RAIL_LDO2:
|
||||
case AXP192_RAIL_LDO3: {
|
||||
if (millivolts < 1800U || millivolts > 3300U) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
uint8_t step = static_cast<uint8_t>((millivolts - 1800U) / 100U);
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, REG_LDO23_VOLTAGE, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
if (rail == AXP192_RAIL_LDO2) {
|
||||
value = static_cast<uint8_t>((value & 0x0FU) | (step << 4U));
|
||||
} else {
|
||||
value = static_cast<uint8_t>((value & 0xF0U) | step);
|
||||
}
|
||||
return i2c_controller_register8_set(parent, address, REG_LDO23_VOLTAGE, value, TIMEOUT);
|
||||
}
|
||||
case AXP192_RAIL_EXTEN:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
error_t axp192_get_battery_voltage(Device* device, uint16_t* millivolts) {
|
||||
uint16_t raw;
|
||||
error_t err = read_adc_raw(device, REG_BATTERY_VOLTAGE, &raw);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*millivolts = static_cast<uint16_t>((raw * 11U) / 10U); // 1.1mV/LSB
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp192_get_battery_charge_current(Device* device, uint16_t* milliamps) {
|
||||
uint16_t raw;
|
||||
error_t err = read_adc_raw(device, REG_CHARGE_CURRENT, &raw);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*milliamps = raw / 2U; // 0.5mA/LSB
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp192_get_battery_discharge_current(Device* device, uint16_t* milliamps) {
|
||||
uint16_t raw;
|
||||
error_t err = read_adc_raw(device, REG_DISCHARGE_CURRENT, &raw);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*milliamps = raw / 2U; // 0.5mA/LSB
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp192_get_battery_power(Device* device, uint32_t* microwatts) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t buffer[3];
|
||||
error_t err = i2c_controller_read_register(parent, address, REG_BATTERY_POWER, buffer, sizeof(buffer), TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
uint32_t raw = (static_cast<uint32_t>(buffer[0]) << 16U) | (static_cast<uint32_t>(buffer[1]) << 8U) | buffer[2];
|
||||
*microwatts = (raw * 11U) / 20U; // 1.1mV * 0.5mA = 0.55uW/LSB
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp192_is_charging(Device* device, bool* charging) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, REG_MODE_CHGSTATUS, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*charging = (value & BIT_CHARGING) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp192_is_charge_enabled(Device* device, bool* enabled) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, REG_CHARGE_CONTROL_1, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*enabled = (value & BIT_CHARGE_ENABLED) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp192_set_charge_enabled(Device* device, bool enabled) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
if (enabled) {
|
||||
return i2c_controller_register8_set_bits(parent, address, REG_CHARGE_CONTROL_1, BIT_CHARGE_ENABLED, TIMEOUT);
|
||||
} else {
|
||||
return i2c_controller_register8_reset_bits(parent, address, REG_CHARGE_CONTROL_1, BIT_CHARGE_ENABLED, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
error_t axp192_power_off(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
return i2c_controller_register8_set_bits(parent, address, REG_SHUTDOWN_BATTERY_CHGLED_CONTROL, BIT_POWER_OFF, TIMEOUT);
|
||||
}
|
||||
|
||||
error_t axp192_set_gpio1_pwm1_output(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
return i2c_controller_register8_set(parent, address, REG_GPIO1_CONTROL, GPIO1_FUNCTION_PWM1, TIMEOUT);
|
||||
}
|
||||
|
||||
error_t axp192_set_pwm1_duty_cycle(Device* device, uint8_t duty) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
return i2c_controller_register8_set(parent, address, REG_PWM1_DUTY_CYCLE_2, duty, TIMEOUT);
|
||||
}
|
||||
|
||||
// region Power supply child device
|
||||
|
||||
static bool ps_supports_property(Device*, PowerSupplyProperty property) {
|
||||
return property == POWER_SUPPLY_PROP_IS_CHARGING ||
|
||||
property == POWER_SUPPLY_PROP_VOLTAGE ||
|
||||
property == POWER_SUPPLY_PROP_CURRENT;
|
||||
}
|
||||
|
||||
static error_t ps_get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) {
|
||||
// device_get_parent() here is the axp192 device itself (this child's parent), not the I2C bus.
|
||||
auto* axp192_device = device_get_parent(device);
|
||||
|
||||
switch (property) {
|
||||
case POWER_SUPPLY_PROP_IS_CHARGING: {
|
||||
bool charging;
|
||||
error_t err = axp192_is_charging(axp192_device, &charging);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
out_value->int_value = charging ? 1 : 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
case POWER_SUPPLY_PROP_VOLTAGE: {
|
||||
uint16_t millivolts;
|
||||
error_t err = axp192_get_battery_voltage(axp192_device, &millivolts);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
out_value->int_value = millivolts;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
case POWER_SUPPLY_PROP_CURRENT: {
|
||||
uint16_t charge_current, discharge_current;
|
||||
error_t err = axp192_get_battery_charge_current(axp192_device, &charge_current);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
err = axp192_get_battery_discharge_current(axp192_device, &discharge_current);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
out_value->int_value = (charge_current > 0U) ? charge_current : -static_cast<int>(discharge_current);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ps_supports_charge_control(Device*) { return true; }
|
||||
|
||||
static bool ps_is_allowed_to_charge(Device* device) {
|
||||
bool enabled = false;
|
||||
axp192_is_charge_enabled(device_get_parent(device), &enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
static error_t ps_set_allowed_to_charge(Device* device, bool allowed) {
|
||||
return axp192_set_charge_enabled(device_get_parent(device), allowed);
|
||||
}
|
||||
|
||||
static bool ps_supports_quick_charge(Device*) { return false; }
|
||||
static bool ps_is_quick_charge_enabled(Device*) { return false; }
|
||||
static error_t ps_set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; }
|
||||
static bool ps_supports_power_off(Device*) { return true; }
|
||||
static error_t ps_power_off(Device* device) { return axp192_power_off(device_get_parent(device)); }
|
||||
|
||||
static constexpr PowerSupplyApi AXP192_POWER_SUPPLY_API = {
|
||||
.supports_property = ps_supports_property,
|
||||
.get_property = ps_get_property,
|
||||
.supports_charge_control = ps_supports_charge_control,
|
||||
.is_allowed_to_charge = ps_is_allowed_to_charge,
|
||||
.set_allowed_to_charge = ps_set_allowed_to_charge,
|
||||
.supports_quick_charge = ps_supports_quick_charge,
|
||||
.is_quick_charge_enabled = ps_is_quick_charge_enabled,
|
||||
.set_quick_charge_enabled = ps_set_quick_charge_enabled,
|
||||
.supports_power_off = ps_supports_power_off,
|
||||
.power_off = ps_power_off,
|
||||
};
|
||||
|
||||
// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal,
|
||||
// but never matched against a devicetree node: axp192_driver wires it up directly by pointer.
|
||||
Driver axp192_power_supply_driver = {
|
||||
.name = "axp192-power-supply",
|
||||
.compatible = (const char*[]) { "axp192-power-supply", nullptr },
|
||||
.start_device = nullptr,
|
||||
.stop_device = nullptr,
|
||||
.api = &AXP192_POWER_SUPPLY_API,
|
||||
.device_type = &POWER_SUPPLY_TYPE,
|
||||
.owner = &axp192_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
struct Axp192Internal {
|
||||
Device* power_supply_device = nullptr;
|
||||
};
|
||||
|
||||
static error_t create_power_supply_child(Device* parent, Device*& out_child) {
|
||||
auto* child = new(std::nothrow) Device { .address = 0, .name = "axp192-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr };
|
||||
if (child == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = device_construct(child);
|
||||
if (error != ERROR_NONE) {
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_parent(child, parent);
|
||||
device_set_driver(child, &axp192_power_supply_driver);
|
||||
|
||||
error = device_add(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
error = device_start(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_remove(child);
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
out_child = child;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static void destroy_power_supply_child(Device* child) {
|
||||
check(device_stop(child) == ERROR_NONE);
|
||||
check(device_remove(child) == ERROR_NONE);
|
||||
check(device_destruct(child) == ERROR_NONE);
|
||||
delete child;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
auto* internal = new(std::nothrow) Axp192Internal();
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = create_power_supply_child(device, internal->power_supply_device);
|
||||
if (error != ERROR_NONE) {
|
||||
delete internal;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Axp192Internal*>(device_get_driver_data(device));
|
||||
destroy_power_supply_child(internal->power_supply_device);
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete internal;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver axp192_driver = {
|
||||
.name = "axp192",
|
||||
.compatible = (const char*[]) { "x-powers,axp192", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = nullptr,
|
||||
.device_type = nullptr,
|
||||
.owner = &axp192_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axp192_backlight.h>
|
||||
#include <axp192_module.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/backlight.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
constexpr auto* TAG = "Axp192Backlight";
|
||||
|
||||
#define GET_CONFIG(device) (static_cast<const Axp192BacklightConfig*>((device)->config))
|
||||
#define GET_INTERNAL(device) (static_cast<Axp192BacklightInternal*>(device_get_driver_data(device)))
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct Axp192BacklightInternal {
|
||||
uint8_t brightness;
|
||||
};
|
||||
|
||||
// region BacklightApi
|
||||
|
||||
static error_t apply_brightness(Device* device, uint8_t brightness) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* axp192 = device_get_parent(device);
|
||||
|
||||
if (brightness == 0) {
|
||||
return axp192_set_rail_enabled(axp192, config->rail, false);
|
||||
}
|
||||
|
||||
uint16_t millivolt = static_cast<uint16_t>(
|
||||
config->min_millivolt +
|
||||
(static_cast<uint32_t>(brightness) * (config->max_millivolt - config->min_millivolt)) / 255U
|
||||
);
|
||||
|
||||
error_t error = axp192_set_rail_voltage(axp192, config->rail, millivolt);
|
||||
if (error != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to set rail voltage");
|
||||
return error;
|
||||
}
|
||||
|
||||
return axp192_set_rail_enabled(axp192, config->rail, true);
|
||||
}
|
||||
|
||||
static error_t axp192_backlight_set_brightness(Device* device, uint8_t brightness) {
|
||||
error_t error = apply_brightness(device, brightness);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
GET_INTERNAL(device)->brightness = brightness;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t axp192_backlight_set_brightness_default(Device* device) {
|
||||
return axp192_backlight_set_brightness(device, GET_CONFIG(device)->brightness_default);
|
||||
}
|
||||
|
||||
static error_t axp192_backlight_get_brightness(Device* device, uint8_t* out_brightness) {
|
||||
*out_brightness = GET_INTERNAL(device)->brightness;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static uint8_t axp192_backlight_get_min_brightness(Device*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t axp192_backlight_get_max_brightness(Device*) {
|
||||
return 255;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static constexpr BacklightApi AXP192_BACKLIGHT_API = {
|
||||
.set_brightness = axp192_backlight_set_brightness,
|
||||
.set_brightness_default = axp192_backlight_set_brightness_default,
|
||||
.get_brightness = axp192_backlight_get_brightness,
|
||||
.get_min_brightness = axp192_backlight_get_min_brightness,
|
||||
.get_max_brightness = axp192_backlight_get_max_brightness,
|
||||
};
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
static error_t start(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
if (config->max_millivolt <= config->min_millivolt) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto* internal = new(std::nothrow) Axp192BacklightInternal { .brightness = 0 };
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
|
||||
error_t error = axp192_backlight_set_brightness_default(device);
|
||||
if (error != ERROR_NONE) {
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete internal;
|
||||
return error;
|
||||
}
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
axp192_backlight_set_brightness(device, 0); // Allowed to fail, we don't care about the result
|
||||
|
||||
auto* internal = GET_INTERNAL(device);
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete internal;
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
Driver axp192_backlight_driver = {
|
||||
.name = "axp192_backlight",
|
||||
.compatible = (const char*[]) { "axp192-backlight", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &AXP192_BACKLIGHT_API,
|
||||
.device_type = &BACKLIGHT_TYPE,
|
||||
.owner = &axp192_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axp192.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver axp192_driver;
|
||||
extern Driver axp192_power_supply_driver;
|
||||
extern Driver axp192_backlight_driver;
|
||||
|
||||
const struct ModuleSymbol axp192_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(axp192_is_rail_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp192_set_rail_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp192_set_rail_voltage),
|
||||
DEFINE_MODULE_SYMBOL(axp192_get_battery_voltage),
|
||||
DEFINE_MODULE_SYMBOL(axp192_get_battery_charge_current),
|
||||
DEFINE_MODULE_SYMBOL(axp192_get_battery_discharge_current),
|
||||
DEFINE_MODULE_SYMBOL(axp192_get_battery_power),
|
||||
DEFINE_MODULE_SYMBOL(axp192_is_charging),
|
||||
DEFINE_MODULE_SYMBOL(axp192_is_charge_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp192_set_charge_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp192_power_off),
|
||||
DEFINE_MODULE_SYMBOL(axp192_set_gpio1_pwm1_output),
|
||||
DEFINE_MODULE_SYMBOL(axp192_set_pwm1_duty_cycle),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&axp192_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&axp192_power_supply_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&axp192_backlight_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&axp192_backlight_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&axp192_power_supply_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&axp192_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module axp192_module = {
|
||||
.name = "axp192",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = axp192_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
|
||||
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
|
||||
|
||||
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
|
||||
|
||||
tactility_add_module(axp2101-module
|
||||
SRCS ${SOURCE_FILES}
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
Apache License
|
||||
==============
|
||||
|
||||
_Version 2.0, January 2004_
|
||||
_<<http://www.apache.org/licenses/>>_
|
||||
|
||||
### Terms and Conditions for use, reproduction, and distribution
|
||||
|
||||
#### 1. Definitions
|
||||
|
||||
“License” shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
“Licensor” shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
“Legal Entity” shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, “control” means **(i)** the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or **(iii)** beneficial ownership of such entity.
|
||||
|
||||
“You” (or “Your”) shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
“Source” form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
“Object” form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
“Work” shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
“Derivative Works” shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
“Contribution” shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
“submitted” means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as “Not a Contribution.”
|
||||
|
||||
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
#### 2. Grant of Copyright License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
#### 3. Grant of Patent License
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
#### 4. Redistribution
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
* **(b)** You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
#### 5. Submission of Contributions
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
#### 6. Trademarks
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
#### 7. Disclaimer of Warranty
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
#### 8. Limitation of Liability
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
#### 9. Accepting Warranty or Additional Liability
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
_END OF TERMS AND CONDITIONS_
|
||||
|
||||
### APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets `[]` replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same “printed page” as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# AXP2101 power management IC
|
||||
|
||||
A driver for the X-Powers `AXP2101` PMIC: DCDC1-5 and ALDO1-4/BLDO1-2/CPUSLDO/DLDO1-2
|
||||
enable and voltage control, battery/VBUS voltage readback, charge enable/status, and
|
||||
system power off. Registers as a `power-supply` child device (voltage, is-charging,
|
||||
charge control, power off) alongside its own public API for full rail control.
|
||||
|
||||
Also includes `axp2101-backlight`: a `BACKLIGHT_TYPE` driver for a backlight wired to one of
|
||||
the AXP2101's LDOs, declared as a devicetree child node of the `axp2101` device it dims. Its
|
||||
`ldo`, `min-millivolt` and `max-millivolt` properties select the channel and map the 0-255
|
||||
brightness level onto that LDO's voltage range; brightness level 0 disables the LDO outright.
|
||||
|
||||
License: [Apache v2.0](LICENSE-Apache-2.0.md)
|
||||
@@ -0,0 +1,24 @@
|
||||
description: >
|
||||
Backlight driven by a switchable/adjustable AXP2101 LDO. Must be declared as a
|
||||
child node of the axp2101 device it dims. Maps the 0-255 brightness level onto
|
||||
the LDO's [min-millivolt,max-millivolt] voltage range; level 0 disables the LDO outright.
|
||||
|
||||
compatible: "axp2101-backlight"
|
||||
|
||||
properties:
|
||||
ldo:
|
||||
type: int
|
||||
required: true
|
||||
description: The Axp2101Ldo powering the backlight (e.g. AXP2101_DLDO1)
|
||||
min-millivolt:
|
||||
type: int
|
||||
default: 0
|
||||
description: LDO voltage at brightness level 0 (the LDO is disabled rather than actually set to this value)
|
||||
max-millivolt:
|
||||
type: int
|
||||
required: true
|
||||
description: LDO voltage at the maximum brightness level (255)
|
||||
brightness-default:
|
||||
type: int
|
||||
default: 255
|
||||
description: Default brightness level, applied by set_brightness_default()
|
||||
@@ -0,0 +1,5 @@
|
||||
description: X-Powers AXP2101 power management IC
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "x-powers,axp2101"
|
||||
@@ -0,0 +1,3 @@
|
||||
dependencies:
|
||||
- TactilityKernel
|
||||
bindings: bindings
|
||||
@@ -0,0 +1,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/module.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
extern struct Module axp2101_module;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axp2101.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(axp2101, struct Axp2101Config)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axp2101_backlight.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
DEFINE_DEVICETREE(axp2101_backlight, struct Axp2101BacklightConfig)
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,96 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
struct Device;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
struct Axp2101Config {
|
||||
/** Address on bus */
|
||||
uint8_t address;
|
||||
};
|
||||
|
||||
/** Switchable/adjustable DCDC (buck) converters of the AXP2101. */
|
||||
enum Axp2101Dcdc {
|
||||
AXP2101_DCDC1,
|
||||
AXP2101_DCDC2,
|
||||
AXP2101_DCDC3,
|
||||
AXP2101_DCDC4,
|
||||
AXP2101_DCDC5,
|
||||
};
|
||||
|
||||
/** Switchable/adjustable LDO regulators of the AXP2101. */
|
||||
enum Axp2101Ldo {
|
||||
AXP2101_ALDO1,
|
||||
AXP2101_ALDO2,
|
||||
AXP2101_ALDO3,
|
||||
AXP2101_ALDO4,
|
||||
AXP2101_BLDO1,
|
||||
AXP2101_BLDO2,
|
||||
AXP2101_CPUSLDO,
|
||||
AXP2101_DLDO1,
|
||||
AXP2101_DLDO2,
|
||||
};
|
||||
|
||||
/** Checks whether a DCDC converter is currently enabled. */
|
||||
error_t axp2101_is_dcdc_enabled(struct Device* device, enum Axp2101Dcdc dcdc, bool* enabled);
|
||||
|
||||
/** Enables or disables a DCDC converter. */
|
||||
error_t axp2101_set_dcdc_enabled(struct Device* device, enum Axp2101Dcdc dcdc, bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Sets the output voltage of a DCDC converter.
|
||||
* @retval ERROR_INVALID_ARGUMENT when millivolts is outside the converter's supported range/step
|
||||
* (DCDC1: 1500-3400mV/100mV; DCDC2: 500-1200mV/10mV or 1220-1540mV/20mV;
|
||||
* DCDC3: 500-1200mV/10mV, 1220-1540mV/20mV or 1600-3400mV/100mV;
|
||||
* DCDC4: 500-1200mV/10mV or 1220-1840mV/20mV; DCDC5: 1200mV, or 1400-3700mV/100mV)
|
||||
*/
|
||||
error_t axp2101_set_dcdc_voltage(struct Device* device, enum Axp2101Dcdc dcdc, uint16_t millivolts);
|
||||
|
||||
/** Checks whether an LDO regulator is currently enabled. */
|
||||
error_t axp2101_is_ldo_enabled(struct Device* device, enum Axp2101Ldo ldo, bool* enabled);
|
||||
|
||||
/** Enables or disables an LDO regulator. */
|
||||
error_t axp2101_set_ldo_enabled(struct Device* device, enum Axp2101Ldo ldo, bool enabled);
|
||||
|
||||
/**
|
||||
* @brief Sets the output voltage of an LDO regulator.
|
||||
* @retval ERROR_INVALID_ARGUMENT when millivolts is outside the regulator's supported range/step
|
||||
* (ALDO1-4/BLDO1-2: 500-3500mV/100mV; CPUSLDO: 500-1400mV/50mV; DLDO1-2: 500-3400mV/100mV)
|
||||
*/
|
||||
error_t axp2101_set_ldo_voltage(struct Device* device, enum Axp2101Ldo ldo, uint16_t millivolts);
|
||||
|
||||
/** Battery voltage in millivolts (0 when no battery is connected). */
|
||||
error_t axp2101_get_battery_voltage(struct Device* device, uint16_t* millivolts);
|
||||
|
||||
/** Whether a battery is currently detected. */
|
||||
error_t axp2101_is_battery_connected(struct Device* device, bool* connected);
|
||||
|
||||
/** Whether VBUS (USB power) is currently present and usable. */
|
||||
error_t axp2101_is_vbus_present(struct Device* device, bool* present);
|
||||
|
||||
/** VBUS voltage in millivolts (0 when VBUS is not present). */
|
||||
error_t axp2101_get_vbus_voltage(struct Device* device, uint16_t* millivolts);
|
||||
|
||||
/** Whether the battery is currently charging. */
|
||||
error_t axp2101_is_charging(struct Device* device, bool* charging);
|
||||
|
||||
/** Whether the charger is allowed to charge the battery. */
|
||||
error_t axp2101_is_charge_enabled(struct Device* device, bool* enabled);
|
||||
|
||||
/** Enables or disables battery charging. */
|
||||
error_t axp2101_set_charge_enabled(struct Device* device, bool enabled);
|
||||
|
||||
/** Powers off the system (does not return on success). */
|
||||
error_t axp2101_power_off(struct Device* device);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,29 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <drivers/axp2101.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Devicetree configuration for a backlight driven by a switchable/adjustable AXP2101 LDO.
|
||||
*/
|
||||
struct Axp2101BacklightConfig {
|
||||
/** The AXP2101 LDO powering the backlight */
|
||||
enum Axp2101Ldo ldo;
|
||||
/** LDO voltage at brightness level 0. Brightness level 0 always disables the LDO outright,
|
||||
* rather than actually driving it to this voltage - see set_brightness() in BacklightApi. */
|
||||
uint16_t min_millivolt;
|
||||
/** LDO voltage at the maximum brightness level (255) */
|
||||
uint16_t max_millivolt;
|
||||
/** Default brightness level, applied by set_brightness_default() */
|
||||
uint8_t brightness_default;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,555 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axp2101.h>
|
||||
#include <axp2101_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/power_supply.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#define TAG "AXP2101"
|
||||
#define GET_CONFIG(device) (static_cast<const Axp2101Config*>((device)->config))
|
||||
|
||||
/** Reference: https://github.com/lewisxhe/XPowersLib (AXP2101 register map and voltage-encoding formulas) */
|
||||
static constexpr uint8_t REG_STATUS1 = 0x00U; // bit5: VBUS good
|
||||
static constexpr uint8_t REG_STATUS2 = 0x01U; // bits[6:5]: charge status (1=charging), bit3: battery connected(?)
|
||||
static constexpr uint8_t REG_COMMON_CONFIG = 0x10U; // bit0: shutdown
|
||||
static constexpr uint8_t REG_CHARGE_GAUGE_WDT_CTRL = 0x18U; // bit1: charge enable
|
||||
static constexpr uint8_t REG_ADC_DATA_RESULT0 = 0x34U; // battery voltage, high 5 bits
|
||||
static constexpr uint8_t REG_ADC_DATA_RESULT1 = 0x35U; // battery voltage, low 8 bits
|
||||
static constexpr uint8_t REG_ADC_DATA_RESULT4 = 0x38U; // VBUS voltage, high 6 bits
|
||||
static constexpr uint8_t REG_ADC_DATA_RESULT5 = 0x39U; // VBUS voltage, low 8 bits
|
||||
static constexpr uint8_t REG_DC_ONOFF_DVM_CTRL = 0x80U; // bits0-4: DCDC1-5 enable
|
||||
static constexpr uint8_t REG_DC_VOL0_CTRL = 0x82U; // DCDC1 voltage; DCDC2-5 follow at +1..+4
|
||||
static constexpr uint8_t REG_LDO_ONOFF_CTRL0 = 0x90U; // bits0-7: ALDO1-4, BLDO1-2, CPUSLDO, DLDO1 enable
|
||||
static constexpr uint8_t REG_LDO_ONOFF_CTRL1 = 0x91U; // bit0: DLDO2 enable
|
||||
static constexpr uint8_t REG_LDO_VOL0_CTRL = 0x92U; // ALDO1 voltage; remaining LDOs follow at +1..+8
|
||||
static constexpr uint8_t REG_ADC_CHANNEL_CTRL = 0x30U; // bit0: battery voltage, bit2: VBUS voltage
|
||||
|
||||
static constexpr uint8_t BIT_VBUS_GOOD = 1U << 5U;
|
||||
static constexpr uint8_t BIT_SHUTDOWN = 1U << 0U;
|
||||
static constexpr uint8_t BIT_CHARGE_ENABLED = 1U << 1U;
|
||||
|
||||
static constexpr TickType_t TIMEOUT = pdMS_TO_TICKS(50);
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Module axp2101_module;
|
||||
|
||||
// region Voltage encoding
|
||||
|
||||
struct Axp2101VoltRange {
|
||||
uint16_t min;
|
||||
uint16_t max;
|
||||
uint16_t step;
|
||||
uint8_t code_base;
|
||||
};
|
||||
|
||||
static error_t encode_ranged_voltage(uint16_t millivolts, const Axp2101VoltRange* ranges, size_t range_count, uint8_t* out_code) {
|
||||
for (size_t i = 0; i < range_count; i++) {
|
||||
const Axp2101VoltRange& range = ranges[i];
|
||||
if (millivolts >= range.min && millivolts <= range.max) {
|
||||
if ((millivolts - range.min) % range.step != 0U) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
*out_code = static_cast<uint8_t>(range.code_base + (millivolts - range.min) / range.step);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
}
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
static error_t write_masked_register(Device* device, uint8_t reg, uint8_t preserve_mask, uint8_t code) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
|
||||
if (preserve_mask == 0U) {
|
||||
return i2c_controller_register8_set(parent, address, reg, code, TIMEOUT);
|
||||
}
|
||||
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, reg, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
value = static_cast<uint8_t>((value & preserve_mask) | code);
|
||||
return i2c_controller_register8_set(parent, address, reg, value, TIMEOUT);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region DCDC
|
||||
|
||||
static error_t get_dcdc_enable_bit(Axp2101Dcdc dcdc, uint8_t* bit) {
|
||||
if (dcdc < AXP2101_DCDC1 || dcdc > AXP2101_DCDC5) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
*bit = static_cast<uint8_t>(1U << dcdc);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_is_dcdc_enabled(Device* device, Axp2101Dcdc dcdc, bool* enabled) {
|
||||
uint8_t bit;
|
||||
error_t err = get_dcdc_enable_bit(dcdc, &bit);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
err = i2c_controller_register8_get(parent, address, REG_DC_ONOFF_DVM_CTRL, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
*enabled = (value & bit) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_set_dcdc_enabled(Device* device, Axp2101Dcdc dcdc, bool enabled) {
|
||||
uint8_t bit;
|
||||
error_t err = get_dcdc_enable_bit(dcdc, &bit);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
if (enabled) {
|
||||
return i2c_controller_register8_set_bits(parent, address, REG_DC_ONOFF_DVM_CTRL, bit, TIMEOUT);
|
||||
} else {
|
||||
return i2c_controller_register8_reset_bits(parent, address, REG_DC_ONOFF_DVM_CTRL, bit, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
error_t axp2101_set_dcdc_voltage(Device* device, Axp2101Dcdc dcdc, uint16_t millivolts) {
|
||||
uint8_t reg = static_cast<uint8_t>(REG_DC_VOL0_CTRL + dcdc);
|
||||
uint8_t code;
|
||||
error_t err;
|
||||
|
||||
switch (dcdc) {
|
||||
case AXP2101_DCDC1: {
|
||||
static constexpr Axp2101VoltRange ranges[] = { { 1500, 3400, 100, 0 } };
|
||||
err = encode_ranged_voltage(millivolts, ranges, 1, &code);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
return write_masked_register(device, reg, 0x00U, code);
|
||||
}
|
||||
case AXP2101_DCDC2: {
|
||||
static constexpr Axp2101VoltRange ranges[] = { { 500, 1200, 10, 0 }, { 1220, 1540, 20, 71 } };
|
||||
err = encode_ranged_voltage(millivolts, ranges, 2, &code);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
return write_masked_register(device, reg, 0x80U, code);
|
||||
}
|
||||
case AXP2101_DCDC3: {
|
||||
static constexpr Axp2101VoltRange ranges[] = { { 500, 1200, 10, 0 }, { 1220, 1540, 20, 71 }, { 1600, 3400, 100, 88 } };
|
||||
err = encode_ranged_voltage(millivolts, ranges, 3, &code);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
return write_masked_register(device, reg, 0x80U, code);
|
||||
}
|
||||
case AXP2101_DCDC4: {
|
||||
static constexpr Axp2101VoltRange ranges[] = { { 500, 1200, 10, 0 }, { 1220, 1840, 20, 71 } };
|
||||
err = encode_ranged_voltage(millivolts, ranges, 2, &code);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
return write_masked_register(device, reg, 0x80U, code);
|
||||
}
|
||||
case AXP2101_DCDC5: {
|
||||
// DCDC5 datasheet quirk: 1200mV maps to a fixed out-of-sequence code, distinct
|
||||
// from the linear 1400-3700mV range (see XPowersLib's setDC5Voltage()).
|
||||
if (millivolts == 1200U) {
|
||||
return write_masked_register(device, reg, 0xE0U, 0x19U);
|
||||
}
|
||||
static constexpr Axp2101VoltRange ranges[] = { { 1400, 3700, 100, 0 } };
|
||||
err = encode_ranged_voltage(millivolts, ranges, 1, &code);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
return write_masked_register(device, reg, 0xE0U, code);
|
||||
}
|
||||
}
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region LDO
|
||||
|
||||
static error_t get_ldo_enable_location(Axp2101Ldo ldo, uint8_t* reg, uint8_t* bit) {
|
||||
if (ldo < AXP2101_ALDO1 || ldo > AXP2101_DLDO2) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
if (ldo == AXP2101_DLDO2) {
|
||||
*reg = REG_LDO_ONOFF_CTRL1;
|
||||
*bit = 1U << 0U;
|
||||
} else {
|
||||
*reg = REG_LDO_ONOFF_CTRL0;
|
||||
*bit = static_cast<uint8_t>(1U << ldo);
|
||||
}
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_is_ldo_enabled(Device* device, Axp2101Ldo ldo, bool* enabled) {
|
||||
uint8_t reg, bit;
|
||||
error_t err = get_ldo_enable_location(ldo, ®, &bit);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
err = i2c_controller_register8_get(parent, address, reg, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
*enabled = (value & bit) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_set_ldo_enabled(Device* device, Axp2101Ldo ldo, bool enabled) {
|
||||
uint8_t reg, bit;
|
||||
error_t err = get_ldo_enable_location(ldo, ®, &bit);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
if (enabled) {
|
||||
return i2c_controller_register8_set_bits(parent, address, reg, bit, TIMEOUT);
|
||||
} else {
|
||||
return i2c_controller_register8_reset_bits(parent, address, reg, bit, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
error_t axp2101_set_ldo_voltage(Device* device, Axp2101Ldo ldo, uint16_t millivolts) {
|
||||
if (ldo < AXP2101_ALDO1 || ldo > AXP2101_DLDO2) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
static constexpr Axp2101VoltRange LDO_RANGE[] = {
|
||||
{ 500, 3500, 100, 0 }, // ALDO1
|
||||
{ 500, 3500, 100, 0 }, // ALDO2
|
||||
{ 500, 3500, 100, 0 }, // ALDO3
|
||||
{ 500, 3500, 100, 0 }, // ALDO4
|
||||
{ 500, 3500, 100, 0 }, // BLDO1
|
||||
{ 500, 3500, 100, 0 }, // BLDO2
|
||||
{ 500, 1400, 50, 0 }, // CPUSLDO
|
||||
{ 500, 3400, 100, 0 }, // DLDO1
|
||||
{ 500, 3400, 100, 0 }, // DLDO2
|
||||
};
|
||||
|
||||
uint8_t code;
|
||||
error_t err = encode_ranged_voltage(millivolts, &LDO_RANGE[ldo], 1, &code);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
|
||||
uint8_t reg = static_cast<uint8_t>(REG_LDO_VOL0_CTRL + ldo);
|
||||
return write_masked_register(device, reg, 0xE0U, code);
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
error_t axp2101_get_battery_voltage(Device* device, uint16_t* millivolts) {
|
||||
bool connected;
|
||||
error_t err = axp2101_is_battery_connected(device, &connected);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
if (!connected) {
|
||||
*millivolts = 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t high, low;
|
||||
err = i2c_controller_register8_get(parent, address, REG_ADC_DATA_RESULT0, &high, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
err = i2c_controller_register8_get(parent, address, REG_ADC_DATA_RESULT1, &low, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*millivolts = static_cast<uint16_t>(((high & 0x1FU) << 8U) | low);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_is_battery_connected(Device* device, bool* connected) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, REG_STATUS1, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*connected = (value & (1U << 3U)) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_is_vbus_present(Device* device, bool* present) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, REG_STATUS1, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*present = (value & BIT_VBUS_GOOD) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_get_vbus_voltage(Device* device, uint16_t* millivolts) {
|
||||
bool present;
|
||||
error_t err = axp2101_is_vbus_present(device, &present);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
if (!present) {
|
||||
*millivolts = 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t high, low;
|
||||
err = i2c_controller_register8_get(parent, address, REG_ADC_DATA_RESULT4, &high, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
err = i2c_controller_register8_get(parent, address, REG_ADC_DATA_RESULT5, &low, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*millivolts = static_cast<uint16_t>(((high & 0x3FU) << 8U) | low);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_is_charging(Device* device, bool* charging) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, REG_STATUS2, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*charging = ((value >> 5U) & 0x03U) == 0x01U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_is_charge_enabled(Device* device, bool* enabled) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
uint8_t value;
|
||||
error_t err = i2c_controller_register8_get(parent, address, REG_CHARGE_GAUGE_WDT_CTRL, &value, TIMEOUT);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
*enabled = (value & BIT_CHARGE_ENABLED) != 0U;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t axp2101_set_charge_enabled(Device* device, bool enabled) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
if (enabled) {
|
||||
return i2c_controller_register8_set_bits(parent, address, REG_CHARGE_GAUGE_WDT_CTRL, BIT_CHARGE_ENABLED, TIMEOUT);
|
||||
} else {
|
||||
return i2c_controller_register8_reset_bits(parent, address, REG_CHARGE_GAUGE_WDT_CTRL, BIT_CHARGE_ENABLED, TIMEOUT);
|
||||
}
|
||||
}
|
||||
|
||||
error_t axp2101_power_off(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
return i2c_controller_register8_set_bits(parent, address, REG_COMMON_CONFIG, BIT_SHUTDOWN, TIMEOUT);
|
||||
}
|
||||
|
||||
// region Power supply child device
|
||||
|
||||
static bool ps_supports_property(Device*, PowerSupplyProperty property) {
|
||||
return property == POWER_SUPPLY_PROP_IS_CHARGING || property == POWER_SUPPLY_PROP_VOLTAGE;
|
||||
}
|
||||
|
||||
static error_t ps_get_property(Device* device, PowerSupplyProperty property, PowerSupplyPropertyValue* out_value) {
|
||||
// device_get_parent() here is the axp2101 device itself (this child's parent), not the I2C bus.
|
||||
auto* axp2101_device = device_get_parent(device);
|
||||
|
||||
switch (property) {
|
||||
case POWER_SUPPLY_PROP_IS_CHARGING: {
|
||||
bool charging;
|
||||
error_t err = axp2101_is_charging(axp2101_device, &charging);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
out_value->int_value = charging ? 1 : 0;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
case POWER_SUPPLY_PROP_VOLTAGE: {
|
||||
uint16_t millivolts;
|
||||
error_t err = axp2101_get_battery_voltage(axp2101_device, &millivolts);
|
||||
if (err != ERROR_NONE) {
|
||||
return err;
|
||||
}
|
||||
out_value->int_value = millivolts;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
default:
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ps_supports_charge_control(Device*) { return true; }
|
||||
|
||||
static bool ps_is_allowed_to_charge(Device* device) {
|
||||
bool enabled = false;
|
||||
axp2101_is_charge_enabled(device_get_parent(device), &enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
static error_t ps_set_allowed_to_charge(Device* device, bool allowed) {
|
||||
return axp2101_set_charge_enabled(device_get_parent(device), allowed);
|
||||
}
|
||||
|
||||
static bool ps_supports_quick_charge(Device*) { return false; }
|
||||
static bool ps_is_quick_charge_enabled(Device*) { return false; }
|
||||
static error_t ps_set_quick_charge_enabled(Device*, bool) { return ERROR_NOT_SUPPORTED; }
|
||||
static bool ps_supports_power_off(Device*) { return true; }
|
||||
static error_t ps_power_off(Device* device) { return axp2101_power_off(device_get_parent(device)); }
|
||||
|
||||
static constexpr PowerSupplyApi AXP2101_POWER_SUPPLY_API = {
|
||||
.supports_property = ps_supports_property,
|
||||
.get_property = ps_get_property,
|
||||
.supports_charge_control = ps_supports_charge_control,
|
||||
.is_allowed_to_charge = ps_is_allowed_to_charge,
|
||||
.set_allowed_to_charge = ps_set_allowed_to_charge,
|
||||
.supports_quick_charge = ps_supports_quick_charge,
|
||||
.is_quick_charge_enabled = ps_is_quick_charge_enabled,
|
||||
.set_quick_charge_enabled = ps_set_quick_charge_enabled,
|
||||
.supports_power_off = ps_supports_power_off,
|
||||
.power_off = ps_power_off,
|
||||
};
|
||||
|
||||
// Registered (driver_construct_add() in module.cpp) so driver_bind() has a valid ->internal,
|
||||
// but never matched against a devicetree node: axp2101_driver wires it up directly by pointer.
|
||||
Driver axp2101_power_supply_driver = {
|
||||
.name = "axp2101-power-supply",
|
||||
.compatible = (const char*[]) { "axp2101-power-supply", nullptr },
|
||||
.start_device = nullptr,
|
||||
.stop_device = nullptr,
|
||||
.api = &AXP2101_POWER_SUPPLY_API,
|
||||
.device_type = &POWER_SUPPLY_TYPE,
|
||||
.owner = &axp2101_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
struct Axp2101Internal {
|
||||
Device* power_supply_device = nullptr;
|
||||
};
|
||||
|
||||
static error_t create_power_supply_child(Device* parent, Device*& out_child) {
|
||||
auto* child = new(std::nothrow) Device { .address = 0, .name = "axp2101-power-supply", .config = nullptr, .parent = nullptr, .internal = nullptr };
|
||||
if (child == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error_t error = device_construct(child);
|
||||
if (error != ERROR_NONE) {
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_parent(child, parent);
|
||||
device_set_driver(child, &axp2101_power_supply_driver);
|
||||
|
||||
error = device_add(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
error = device_start(child);
|
||||
if (error != ERROR_NONE) {
|
||||
device_remove(child);
|
||||
device_destruct(child);
|
||||
delete child;
|
||||
return error;
|
||||
}
|
||||
|
||||
out_child = child;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static void destroy_power_supply_child(Device* child) {
|
||||
check(device_stop(child) == ERROR_NONE);
|
||||
check(device_remove(child) == ERROR_NONE);
|
||||
check(device_destruct(child) == ERROR_NONE);
|
||||
delete child;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
auto address = GET_CONFIG(device)->address;
|
||||
// Battery/VBUS voltage ADC channels are off by default; axp2101_get_battery_voltage()
|
||||
// and axp2101_get_vbus_voltage() need them on to read anything but 0.
|
||||
error_t error = i2c_controller_register8_set_bits(parent, address, REG_ADC_CHANNEL_CTRL, (1U << 0U) | (1U << 2U), TIMEOUT);
|
||||
if (error != ERROR_NONE) {
|
||||
LOG_W(TAG, "Failed to enable battery/VBUS ADC channels");
|
||||
return error;
|
||||
}
|
||||
|
||||
auto* internal = new(std::nothrow) Axp2101Internal();
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
error = create_power_supply_child(device, internal->power_supply_device);
|
||||
if (error != ERROR_NONE) {
|
||||
delete internal;
|
||||
return error;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Axp2101Internal*>(device_get_driver_data(device));
|
||||
destroy_power_supply_child(internal->power_supply_device);
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete internal;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Driver axp2101_driver = {
|
||||
.name = "axp2101",
|
||||
.compatible = (const char*[]) { "x-powers,axp2101", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = nullptr,
|
||||
.device_type = nullptr,
|
||||
.owner = &axp2101_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axp2101_backlight.h>
|
||||
#include <axp2101_module.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/backlight.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
#define TAG "Axp2101Backlight"
|
||||
#define GET_CONFIG(device) (static_cast<const Axp2101BacklightConfig*>((device)->config))
|
||||
#define GET_INTERNAL(device) (static_cast<Axp2101BacklightInternal*>(device_get_driver_data(device)))
|
||||
|
||||
extern "C" {
|
||||
|
||||
struct Axp2101BacklightInternal {
|
||||
uint8_t brightness;
|
||||
};
|
||||
|
||||
// region BacklightApi
|
||||
|
||||
// Step size of axp2101_set_ldo_voltage()'s underlying register encoding (see axp2101.cpp's LDO_RANGE table).
|
||||
static uint16_t get_ldo_voltage_step(Axp2101Ldo ldo) {
|
||||
return ldo == AXP2101_CPUSLDO ? 50U : 100U;
|
||||
}
|
||||
|
||||
static error_t apply_brightness(Device* device, uint8_t brightness) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* axp2101 = device_get_parent(device);
|
||||
|
||||
if (brightness == 0) {
|
||||
return axp2101_set_ldo_enabled(axp2101, config->ldo, false);
|
||||
}
|
||||
|
||||
uint16_t step = get_ldo_voltage_step(config->ldo);
|
||||
uint16_t raw_millivolt = static_cast<uint16_t>(
|
||||
config->min_millivolt +
|
||||
(static_cast<uint32_t>(brightness) * (config->max_millivolt - config->min_millivolt)) / 255U
|
||||
);
|
||||
// Round to the nearest valid step; the LDO's voltage range always starts at a multiple of every
|
||||
// supported step, so rounding from zero keeps the result on a valid boundary.
|
||||
uint16_t millivolt = static_cast<uint16_t>(((raw_millivolt + step / 2U) / step) * step);
|
||||
if (millivolt < config->min_millivolt) {
|
||||
millivolt = config->min_millivolt;
|
||||
} else if (millivolt > config->max_millivolt) {
|
||||
millivolt = config->max_millivolt;
|
||||
}
|
||||
|
||||
error_t error = axp2101_set_ldo_voltage(axp2101, config->ldo, millivolt);
|
||||
if (error != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to set LDO voltage");
|
||||
return error;
|
||||
}
|
||||
|
||||
return axp2101_set_ldo_enabled(axp2101, config->ldo, true);
|
||||
}
|
||||
|
||||
static error_t axp2101_backlight_set_brightness(Device* device, uint8_t brightness) {
|
||||
error_t error = apply_brightness(device, brightness);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
GET_INTERNAL(device)->brightness = brightness;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t axp2101_backlight_set_brightness_default(Device* device) {
|
||||
return axp2101_backlight_set_brightness(device, GET_CONFIG(device)->brightness_default);
|
||||
}
|
||||
|
||||
static error_t axp2101_backlight_get_brightness(Device* device, uint8_t* out_brightness) {
|
||||
*out_brightness = GET_INTERNAL(device)->brightness;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static uint8_t axp2101_backlight_get_min_brightness(Device*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static uint8_t axp2101_backlight_get_max_brightness(Device*) {
|
||||
return 255;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static constexpr BacklightApi AXP2101_BACKLIGHT_API = {
|
||||
.set_brightness = axp2101_backlight_set_brightness,
|
||||
.set_brightness_default = axp2101_backlight_set_brightness_default,
|
||||
.get_brightness = axp2101_backlight_get_brightness,
|
||||
.get_min_brightness = axp2101_backlight_get_min_brightness,
|
||||
.get_max_brightness = axp2101_backlight_get_max_brightness,
|
||||
};
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
static error_t start(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
if (config->max_millivolt <= config->min_millivolt) {
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
auto* internal = new(std::nothrow) Axp2101BacklightInternal { .brightness = 0 };
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
|
||||
axp2101_backlight_set_brightness_default(device); // Allowed to fail, we don't care about the result
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
axp2101_backlight_set_brightness(device, 0); // Allowed to fail, we don't care about the result
|
||||
|
||||
auto* internal = GET_INTERNAL(device);
|
||||
device_set_driver_data(device, nullptr);
|
||||
delete internal;
|
||||
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
Driver axp2101_backlight_driver = {
|
||||
.name = "axp2101_backlight",
|
||||
.compatible = (const char*[]) { "axp2101-backlight", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &AXP2101_BACKLIGHT_API,
|
||||
.device_type = &BACKLIGHT_TYPE,
|
||||
.owner = &axp2101_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axp2101.h>
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver axp2101_driver;
|
||||
extern Driver axp2101_power_supply_driver;
|
||||
extern Driver axp2101_backlight_driver;
|
||||
|
||||
const struct ModuleSymbol axp2101_module_symbols[] = {
|
||||
DEFINE_MODULE_SYMBOL(axp2101_is_dcdc_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_set_dcdc_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_set_dcdc_voltage),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_is_ldo_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_set_ldo_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_set_ldo_voltage),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_get_battery_voltage),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_is_battery_connected),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_is_vbus_present),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_get_vbus_voltage),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_is_charging),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_is_charge_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_set_charge_enabled),
|
||||
DEFINE_MODULE_SYMBOL(axp2101_power_off),
|
||||
MODULE_SYMBOL_TERMINATOR
|
||||
};
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&axp2101_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&axp2101_power_supply_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&axp2101_backlight_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&axp2101_backlight_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&axp2101_power_supply_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&axp2101_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module axp2101_module = {
|
||||
.name = "axp2101",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = axp2101_module_symbols,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
description: AXS15231B touch controller (I2C side of the combined display+touch chip)
|
||||
|
||||
include: ["i2c-device.yaml"]
|
||||
|
||||
compatible: "axs,axs15231b-touch"
|
||||
|
||||
bus: i2c
|
||||
|
||||
properties:
|
||||
x-max:
|
||||
type: int
|
||||
required: true
|
||||
description: Maximum X coordinate reported by the controller (typically the panel's horizontal resolution)
|
||||
y-max:
|
||||
type: int
|
||||
required: true
|
||||
description: Maximum Y coordinate reported by the controller (typically the panel's vertical resolution)
|
||||
swap-xy:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Swap the X and Y axes
|
||||
mirror-x:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the X axis
|
||||
mirror-y:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the Y axis
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Reset GPIO pin
|
||||
pin-interrupt:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Interrupt GPIO pin
|
||||
reset-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the reset pin is active high
|
||||
interrupt-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the interrupt pin is active high
|
||||
@@ -1,75 +0,0 @@
|
||||
description: >
|
||||
AXS15231B display panel (QSPI interface). Combined display+touch controller chip - see
|
||||
axs,axs15231b-touch for the touch side, which sits on a separate I2C bus and is modeled as an
|
||||
independent devicetree node.
|
||||
|
||||
compatible: "axs,axs15231b"
|
||||
|
||||
bus: spi
|
||||
|
||||
properties:
|
||||
horizontal-resolution:
|
||||
type: int
|
||||
required: true
|
||||
description: Horizontal resolution in pixels
|
||||
vertical-resolution:
|
||||
type: int
|
||||
required: true
|
||||
description: Vertical resolution in pixels
|
||||
mirror-x:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the X axis
|
||||
mirror-y:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Mirror the Y axis
|
||||
invert-color:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Invert the panel's color output
|
||||
bgr-order:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Use BGR element order instead of RGB
|
||||
pixel-clock-hz:
|
||||
type: int
|
||||
default: 40000000
|
||||
description: QSPI pixel clock frequency in Hz
|
||||
transaction-queue-depth:
|
||||
type: int
|
||||
default: 10
|
||||
description: Size of the internal SPI transaction queue
|
||||
pin-reset:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Reset GPIO pin
|
||||
reset-active-high:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Whether the reset pin is active high
|
||||
pin-te:
|
||||
type: phandles
|
||||
default: GPIO_PIN_SPEC_NONE
|
||||
description: Optional Tearing-Effect GPIO pin. When set, draw_bitmap waits (best-effort, up
|
||||
to 20ms) for a V-blank pulse on this pin before starting each transfer, to reduce visible
|
||||
tearing. Omit to skip TE sync entirely.
|
||||
init-sequence:
|
||||
type: array
|
||||
element-type: uint8_t
|
||||
description: >
|
||||
Custom vendor bring-up sequence, flattened into bytes as a run of
|
||||
[cmd, data-length, delay-ms, data-length bytes of data...] entries. Omit to use the
|
||||
AXS15231B component's own built-in default sequence.
|
||||
requires-full-frame:
|
||||
type: boolean
|
||||
default: false
|
||||
description: >
|
||||
Whether this panel needs full-frame-only draws (DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME) -
|
||||
a sub-region draw_bitmap() call desyncs this chip's row auto-increment counter, since its
|
||||
QSPI command set has no row-address command.
|
||||
It's not certain that this is required for all driver implementations, so it's a config option for now.
|
||||
backlight:
|
||||
type: phandle
|
||||
default: "NULL"
|
||||
description: Optional reference to this display's backlight device
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axs15231b_display.h>
|
||||
|
||||
// The devicetree compiler derives the expected config typedef name from the compatible
|
||||
// string's suffix (e.g. "axs,axs15231b" -> axs15231b_config_dt), not from the node name or
|
||||
// driver name, so the tag here must match that exactly.
|
||||
DEFINE_DEVICETREE(axs15231b, struct Axs15231bDisplayConfig)
|
||||
@@ -1,10 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/bindings/bindings.h>
|
||||
#include <drivers/axs15231b_touch.h>
|
||||
|
||||
// The devicetree compiler derives the expected config typedef name from the compatible
|
||||
// string's suffix (e.g. "axs,axs15231b-touch" -> axs15231b_touch_config_dt), not from the node
|
||||
// name or driver name, so the tag here must match that exactly.
|
||||
DEFINE_DEVICETREE(axs15231b_touch, struct Axs15231bTouchConfig)
|
||||
@@ -1,48 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Axs15231bDisplayConfig {
|
||||
uint16_t horizontal_resolution;
|
||||
uint16_t vertical_resolution;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
bool invert_color;
|
||||
bool bgr_order;
|
||||
uint32_t pixel_clock_hz;
|
||||
uint32_t transaction_queue_depth;
|
||||
|
||||
// Reset pin. GPIO_PIN_SPEC_NONE means no reset line is wired up (matches the original
|
||||
// deprecated-HAL config for the boards using this chip so far).
|
||||
struct GpioPinSpec pin_reset;
|
||||
bool reset_active_high;
|
||||
|
||||
// Optional Tearing-Effect GPIO pin. When set (not GPIO_PIN_SPEC_NONE), draw_bitmap() waits
|
||||
// (best-effort, up to 20ms) for a V-blank pulse on this pin before starting each transfer, to
|
||||
// reduce visible tearing. GPIO_PIN_SPEC_NONE skips TE sync entirely.
|
||||
struct GpioPinSpec pin_te;
|
||||
|
||||
// Custom vendor init sequence, flattened as bytes: a run of
|
||||
// [cmd, data_len, delay_ms, data_len bytes of data...] entries. NULL/0 falls back to the
|
||||
// AXS15231B component's own built-in default sequence.
|
||||
const uint8_t* init_sequence;
|
||||
uint32_t init_sequence_length;
|
||||
|
||||
bool requires_full_frame;
|
||||
|
||||
// Optional reference to this display's backlight device, NULL if none.
|
||||
struct Device* backlight;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,31 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/drivers/gpio.h>
|
||||
|
||||
struct Axs15231bTouchConfig {
|
||||
// Devicetree address hint. Unused by the driver: the AXS15231B always sits at a fixed
|
||||
// I2C address (see ESP_LCD_TOUCH_IO_I2C_AXS15231B_ADDRESS in esp_lcd_axs15231b.h).
|
||||
uint8_t address;
|
||||
uint16_t x_max;
|
||||
uint16_t y_max;
|
||||
bool swap_xy;
|
||||
bool mirror_x;
|
||||
bool mirror_y;
|
||||
struct GpioPinSpec pin_reset;
|
||||
struct GpioPinSpec pin_interrupt;
|
||||
bool reset_active_high;
|
||||
bool interrupt_active_high;
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,501 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axs15231b_display.h>
|
||||
#include <axs15231b_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/drivers/esp32_spi.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_lcd_axs15231b.h>
|
||||
#include <esp_lcd_io_spi.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_panel_ops.h>
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "AXS15231B"
|
||||
#define GET_CONFIG(device) (static_cast<const Axs15231bDisplayConfig*>((device)->config))
|
||||
|
||||
struct Axs15231bDisplayInternal {
|
||||
esp_lcd_panel_io_handle_t io_handle;
|
||||
esp_lcd_panel_handle_t panel_handle;
|
||||
// Given from ISR context by on_color_trans_done() once a queued transfer physically
|
||||
// completes. draw_bitmap() blocks on this so it can honor DisplayApi's synchronous contract
|
||||
// (see lvgl_display.c: the caller reuses/overwrites the color buffer as soon as draw_bitmap
|
||||
// returns) - esp_lcd_panel_draw_bitmap() itself only queues the transfer and returns early.
|
||||
SemaphoreHandle_t draw_done_semaphore;
|
||||
// Non-null only when a TE (Tearing-Effect) pin is configured. Signaled by te_isr_handler()
|
||||
// on each rising edge, so draw_bitmap() can wait for the next V-blank before transferring.
|
||||
SemaphoreHandle_t te_semaphore;
|
||||
bool te_isr_installed;
|
||||
// Whether we're the one who called gpio_install_isr_service() - if so, we must be the one to
|
||||
// uninstall it, but only if no other pin on the system is still relying on it.
|
||||
bool te_isr_service_installed_by_us;
|
||||
axs15231b_lcd_init_cmd_t* parsed_init_cmds;
|
||||
};
|
||||
|
||||
static bool IRAM_ATTR on_color_trans_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void* user_ctx) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(user_ctx);
|
||||
BaseType_t high_task_woken = pdFALSE;
|
||||
xSemaphoreGiveFromISR(internal->draw_done_semaphore, &high_task_woken);
|
||||
return high_task_woken == pdTRUE;
|
||||
}
|
||||
|
||||
static void IRAM_ATTR te_isr_handler(void* arg) {
|
||||
auto* semaphore = static_cast<SemaphoreHandle_t>(arg);
|
||||
BaseType_t high_task_woken = pdFALSE;
|
||||
xSemaphoreGiveFromISR(semaphore, &high_task_woken);
|
||||
if (high_task_woken == pdTRUE) {
|
||||
portYIELD_FROM_ISR();
|
||||
}
|
||||
}
|
||||
|
||||
static int pin_or_unused(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
|
||||
}
|
||||
|
||||
static gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
// Unpacks the devicetree's flat [cmd, data_len, delay_ms, data_len bytes...] encoding (produced
|
||||
// by the devicetree compiler's "array" property type - see init-sequence in
|
||||
// bindings/axs,axs15231b.yaml) into a heap-allocated axs15231b_lcd_init_cmd_t array.
|
||||
static bool parse_init_sequence(const uint8_t* bytes, uint32_t length, axs15231b_lcd_init_cmd_t** out_cmds, uint16_t* out_count) {
|
||||
uint32_t count = 0;
|
||||
for (uint32_t offset = 0; offset < length; count++) {
|
||||
if (offset + 3 > length) {
|
||||
LOG_E(TAG, "init-sequence truncated: entry header runs past the end of the array");
|
||||
return false;
|
||||
}
|
||||
offset += 3 + bytes[offset + 1];
|
||||
if (offset > length) {
|
||||
LOG_E(TAG, "init-sequence truncated: entry data runs past the end of the array");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto* cmds = static_cast<axs15231b_lcd_init_cmd_t*>(malloc(count * sizeof(axs15231b_lcd_init_cmd_t)));
|
||||
if (cmds == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t offset = 0;
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint8_t data_len = bytes[offset + 1];
|
||||
cmds[i] = {
|
||||
.cmd = bytes[offset],
|
||||
.data = data_len > 0 ? &bytes[offset + 3] : nullptr,
|
||||
.data_bytes = data_len,
|
||||
.delay_ms = bytes[offset + 2],
|
||||
};
|
||||
offset += 3 + data_len;
|
||||
}
|
||||
|
||||
*out_cmds = cmds;
|
||||
*out_count = (uint16_t)count;
|
||||
return true;
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// Best-effort: a TE pin is a hardware refinement, not a requirement, so failures here are logged
|
||||
// and left for the caller to treat as non-fatal (matches the original deprecated-HAL driver,
|
||||
// which continued without TE sync if setup failed).
|
||||
static bool setup_te_sync(Axs15231bDisplayInternal* internal, gpio_num_t te_pin) {
|
||||
if (te_pin == GPIO_NUM_NC) {
|
||||
return true;
|
||||
}
|
||||
|
||||
internal->te_semaphore = xSemaphoreCreateBinary();
|
||||
if (internal->te_semaphore == nullptr) {
|
||||
LOG_E(TAG, "Failed to create TE sync semaphore");
|
||||
return false;
|
||||
}
|
||||
|
||||
gpio_config_t io_conf = {
|
||||
.pin_bit_mask = 1ULL << te_pin,
|
||||
.mode = GPIO_MODE_INPUT,
|
||||
.pull_up_en = GPIO_PULLUP_DISABLE,
|
||||
.pull_down_en = GPIO_PULLDOWN_DISABLE,
|
||||
.intr_type = GPIO_INTR_POSEDGE,
|
||||
};
|
||||
if (gpio_config(&io_conf) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to configure TE GPIO");
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
esp_err_t ret = gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
|
||||
if (ret == ESP_OK) {
|
||||
internal->te_isr_service_installed_by_us = true;
|
||||
} else if (ret != ESP_ERR_INVALID_STATE) { // ESP_ERR_INVALID_STATE means it's already installed elsewhere
|
||||
LOG_E(TAG, "Failed to install GPIO ISR service");
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gpio_isr_handler_add(te_pin, te_isr_handler, internal->te_semaphore) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to add TE ISR handler");
|
||||
if (internal->te_isr_service_installed_by_us) {
|
||||
gpio_uninstall_isr_service();
|
||||
internal->te_isr_service_installed_by_us = false;
|
||||
}
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
internal->te_isr_installed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static void teardown_te_sync(Axs15231bDisplayInternal* internal, gpio_num_t te_pin) {
|
||||
if (internal->te_isr_installed) {
|
||||
gpio_isr_handler_remove(te_pin);
|
||||
gpio_intr_disable(te_pin);
|
||||
internal->te_isr_installed = false;
|
||||
}
|
||||
if (internal->te_isr_service_installed_by_us) {
|
||||
gpio_uninstall_isr_service();
|
||||
internal->te_isr_service_installed_by_us = false;
|
||||
}
|
||||
if (internal->te_semaphore != nullptr) {
|
||||
vSemaphoreDelete(internal->te_semaphore);
|
||||
internal->te_semaphore = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &SPI_CONTROLLER_TYPE);
|
||||
|
||||
const auto* spi_config = static_cast<const Esp32SpiConfig*>(parent->config);
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
struct GpioPinSpec cs_pin;
|
||||
if (esp32_spi_get_cs_pin(device, &cs_pin) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to resolve CS pin");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(malloc(sizeof(Axs15231bDisplayInternal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
internal->te_semaphore = nullptr;
|
||||
internal->te_isr_installed = false;
|
||||
internal->te_isr_service_installed_by_us = false;
|
||||
internal->parsed_init_cmds = nullptr;
|
||||
|
||||
const axs15231b_lcd_init_cmd_t* init_cmds = nullptr;
|
||||
uint16_t init_cmds_size = 0;
|
||||
if (config->init_sequence != nullptr && config->init_sequence_length > 0) {
|
||||
if (!parse_init_sequence(config->init_sequence, config->init_sequence_length, &internal->parsed_init_cmds, &init_cmds_size)) {
|
||||
LOG_E(TAG, "Failed to parse init-sequence property");
|
||||
free(internal);
|
||||
return ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
init_cmds = internal->parsed_init_cmds;
|
||||
}
|
||||
|
||||
internal->draw_done_semaphore = xSemaphoreCreateBinary();
|
||||
if (internal->draw_done_semaphore == nullptr) {
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
// AXS15231B is only ever driven over QSPI in this codebase (no plain-SPI/i80 board has shown
|
||||
// up yet), so quad_mode/lcd_cmd_bits/lcd_param_bits are fixed rather than devicetree knobs -
|
||||
// matches AXS15231B_PANEL_IO_QSPI_CONFIG() in esp_lcd_axs15231b.h. dc_gpio_num is unused in
|
||||
// QSPI mode (command/data framing goes over the command byte instead of a DC line).
|
||||
esp_lcd_panel_io_spi_config_t io_config = {
|
||||
.cs_gpio_num = pin_or_unused(cs_pin),
|
||||
.dc_gpio_num = -1,
|
||||
.spi_mode = 3,
|
||||
.pclk_hz = config->pixel_clock_hz,
|
||||
.trans_queue_depth = config->transaction_queue_depth,
|
||||
.on_color_trans_done = on_color_trans_done,
|
||||
.user_ctx = internal,
|
||||
.lcd_cmd_bits = 32,
|
||||
.lcd_param_bits = 8,
|
||||
.cs_ena_pretrans = 0,
|
||||
.cs_ena_posttrans = 0,
|
||||
.flags = {
|
||||
.dc_high_on_cmd = 0,
|
||||
.dc_low_on_data = 0,
|
||||
.dc_low_on_param = 0,
|
||||
.octal_mode = 0,
|
||||
.quad_mode = 1,
|
||||
.sio_mode = 0,
|
||||
.lsb_first = 0,
|
||||
.cs_high_active = 0,
|
||||
},
|
||||
};
|
||||
|
||||
esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_config->host, &io_config, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret));
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
axs15231b_vendor_config_t vendor_config = {
|
||||
.init_cmds = init_cmds,
|
||||
.init_cmds_size = init_cmds_size,
|
||||
.flags = {
|
||||
.use_qspi_interface = 1,
|
||||
},
|
||||
};
|
||||
|
||||
esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = pin_or_unused(config->pin_reset),
|
||||
.rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
|
||||
.bits_per_pixel = 16,
|
||||
.flags = { .reset_active_high = config->reset_active_high },
|
||||
.vendor_config = &vendor_config,
|
||||
};
|
||||
|
||||
ret = esp_lcd_new_panel_axs15231b(internal->io_handle, &panel_config, &internal->panel_handle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret));
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// Bring-up sequence. swap_xy is intentionally not called (and not exposed in DisplayApi below):
|
||||
// It doesn't work on this chip/panel combination.
|
||||
//
|
||||
// esp_lcd_axs15231b's disp_on_off callback is wired up backwards: its body branches on a
|
||||
// parameter it names "off" (true -> DISPOFF, false -> DISPON), but esp_lcd_panel_disp_on_off()
|
||||
// forwards its "on_off" argument straight through with no inversion - so passing true here
|
||||
// actually switches the panel OFF. Pass false to really turn it on (confirmed against the
|
||||
// deleted deprecated-HAL driver, which called this same function with false for the same
|
||||
// reason). See axs15231b_disp_on_off() below, which un-inverts this for DisplayApi callers.
|
||||
//
|
||||
// (note: all of this was tested on guition-jc3248w535c only)
|
||||
bool ok =
|
||||
esp_lcd_panel_reset(internal->panel_handle) == ESP_OK &&
|
||||
esp_lcd_panel_init(internal->panel_handle) == ESP_OK &&
|
||||
esp_lcd_panel_mirror(internal->panel_handle, config->mirror_x, config->mirror_y) == ESP_OK &&
|
||||
esp_lcd_panel_invert_color(internal->panel_handle, config->invert_color) == ESP_OK &&
|
||||
esp_lcd_panel_disp_on_off(internal->panel_handle, false) == ESP_OK;
|
||||
|
||||
if (!ok) {
|
||||
LOG_E(TAG, "Failed to bring up panel");
|
||||
esp_lcd_panel_del(internal->panel_handle);
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (!setup_te_sync(internal, pin_or_nc(config->pin_te))) {
|
||||
LOG_W(TAG, "TE sync setup failed, continuing without TE synchronization");
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
const auto* config = GET_CONFIG(device);
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
|
||||
teardown_te_sync(internal, pin_or_nc(config->pin_te));
|
||||
|
||||
if (internal->panel_handle != nullptr) {
|
||||
if (esp_lcd_panel_del(internal->panel_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete panel");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->panel_handle = nullptr;
|
||||
}
|
||||
|
||||
if (internal->io_handle != nullptr) {
|
||||
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete panel IO");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->io_handle = nullptr;
|
||||
}
|
||||
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal->parsed_init_cmds);
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region DisplayApi
|
||||
|
||||
static error_t axs15231b_reset(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_init(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_init(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
|
||||
// Best-effort wait for the next V-blank pulse before transferring, to reduce visible tearing.
|
||||
// Non-fatal if it times out (matches the original deprecated-HAL driver) - draw_bitmap()
|
||||
// still has to happen even if TE sync missed its window.
|
||||
if (internal->te_semaphore != nullptr) {
|
||||
xSemaphoreTake(internal->te_semaphore, 0); // drain any already-pending signal
|
||||
xSemaphoreTake(internal->te_semaphore, pdMS_TO_TICKS(20));
|
||||
}
|
||||
|
||||
// Drain any stale signal left over from a prior non-draw transaction (bring-up commands like
|
||||
// reset/init also complete through on_color_trans_done), so the take() below can only be
|
||||
// satisfied by this draw's own transfer completing.
|
||||
xSemaphoreTake(internal->draw_done_semaphore, 0);
|
||||
|
||||
if (esp_lcd_panel_draw_bitmap(internal->panel_handle, x_start, y_start, x_end, y_end, color_data) != ESP_OK) {
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
xSemaphoreTake(internal->draw_done_semaphore, portMAX_DELAY);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_mirror(Device* device, bool x_axis, bool y_axis) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_mirror(internal->panel_handle, x_axis, y_axis) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static bool axs15231b_get_mirror_x(Device* device) {
|
||||
return GET_CONFIG(device)->mirror_x;
|
||||
}
|
||||
|
||||
static bool axs15231b_get_mirror_y(Device* device) {
|
||||
return GET_CONFIG(device)->mirror_y;
|
||||
}
|
||||
|
||||
// swap_xy/set_gap/disp_sleep are not exposed: swap_xy is confirmed non-functional on this
|
||||
// chip/panel combination on real hardware (see start()'s comment), and the AXS15231B component
|
||||
// doesn't implement set_gap or disp_sleep at all.
|
||||
|
||||
static error_t axs15231b_invert_color(Device* device, bool invert_color_data) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_panel_invert_color(internal->panel_handle, invert_color_data) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_disp_on_off(Device* device, bool on_off) {
|
||||
auto* internal = static_cast<Axs15231bDisplayInternal*>(device_get_driver_data(device));
|
||||
// Inverted: see the comment on the disp_on_off call in start() above.
|
||||
return esp_lcd_panel_disp_on_off(internal->panel_handle, !on_off) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// _SWAPPED (not plain RGB565): the panel expects each 16-bit pixel high-byte-first over the QSPI
|
||||
// bus, but this CPU is little-endian - the original deprecated-HAL driver did the equivalent
|
||||
// byte-swap by hand in its custom flush callback (lv_draw_sw_rgb565_swap()); the generic
|
||||
// lvgl-module bridge does it for us once we report this format (see lvgl_display_map_color_format()).
|
||||
static enum DisplayColorFormat axs15231b_get_color_format(Device*) {
|
||||
return DISPLAY_COLOR_FORMAT_RGB565_SWAPPED;
|
||||
}
|
||||
|
||||
static uint16_t axs15231b_get_resolution_x(Device* device) {
|
||||
return GET_CONFIG(device)->horizontal_resolution;
|
||||
}
|
||||
|
||||
static uint16_t axs15231b_get_resolution_y(Device* device) {
|
||||
return GET_CONFIG(device)->vertical_resolution;
|
||||
}
|
||||
|
||||
static void axs15231b_get_frame_buffer(Device*, uint8_t, void** out_buffer) {
|
||||
*out_buffer = nullptr;
|
||||
}
|
||||
|
||||
static uint8_t axs15231b_get_frame_buffer_count(Device*) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static error_t axs15231b_get_backlight(Device* device, Device** backlight) {
|
||||
auto* configured_backlight = GET_CONFIG(device)->backlight;
|
||||
if (configured_backlight == nullptr) {
|
||||
return ERROR_NOT_SUPPORTED;
|
||||
}
|
||||
*backlight = configured_backlight;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// REQUIRES_FULL_FRAME is the only capability that varies per device instance (via the
|
||||
// requires-full-frame devicetree property, see axs15231b_display.h) - a sub-region draw_bitmap()
|
||||
// call desyncs this chip's row auto-increment counter, since its QSPI command set has no
|
||||
// row-address command, but that's been confirmed needed on some boards' wiring/panel combination
|
||||
// and not assumed true for every AXS15231B board (see start()'s notes on what's been tested where).
|
||||
// Every other bit stays fixed for the driver, mirrored here from axs15231b_display_api.capabilities.
|
||||
static bool axs15231b_has_capability(Device* device, uint32_t capability) {
|
||||
uint32_t static_capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR |
|
||||
DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT;
|
||||
if (GET_CONFIG(device)->requires_full_frame) {
|
||||
static_capabilities |= DISPLAY_CAPABILITY_REQUIRES_FULL_FRAME;
|
||||
}
|
||||
return (static_capabilities & capability) == capability;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static const DisplayApi axs15231b_display_api = {
|
||||
// Mirrors axs15231b_has_capability()'s static_capabilities for callers that read this field
|
||||
// directly instead of going through display_has_capability()/has_capability(). Excludes
|
||||
// REQUIRES_FULL_FRAME - see axs15231b_has_capability() above, which is the source of truth.
|
||||
.capabilities = DISPLAY_CAPABILITY_CAP_MIRROR | DISPLAY_CAPABILITY_INVERT_COLOR |
|
||||
DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_BACKLIGHT,
|
||||
.reset = axs15231b_reset,
|
||||
.init = axs15231b_init,
|
||||
.draw_bitmap = axs15231b_draw_bitmap,
|
||||
.mirror = axs15231b_mirror,
|
||||
.swap_xy = nullptr,
|
||||
.get_swap_xy = nullptr,
|
||||
.get_mirror_x = axs15231b_get_mirror_x,
|
||||
.get_mirror_y = axs15231b_get_mirror_y,
|
||||
.set_gap = nullptr,
|
||||
.invert_color = axs15231b_invert_color,
|
||||
.disp_on_off = axs15231b_disp_on_off,
|
||||
.disp_sleep = nullptr,
|
||||
.get_color_format = axs15231b_get_color_format,
|
||||
.get_resolution_x = axs15231b_get_resolution_x,
|
||||
.get_resolution_y = axs15231b_get_resolution_y,
|
||||
.get_frame_buffer = axs15231b_get_frame_buffer,
|
||||
.get_frame_buffer_count = axs15231b_get_frame_buffer_count,
|
||||
.get_backlight = axs15231b_get_backlight,
|
||||
.has_capability = axs15231b_has_capability,
|
||||
};
|
||||
|
||||
Driver axs15231b_display_driver = {
|
||||
.name = "axs15231b_display",
|
||||
.compatible = (const char*[]) { "axs,axs15231b", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &axs15231b_display_api,
|
||||
.device_type = &DISPLAY_TYPE,
|
||||
.owner = &axs15231b_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -1,211 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <drivers/axs15231b_touch.h>
|
||||
#include <axs15231b_module.h>
|
||||
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/esp32_i2c.h>
|
||||
#include <tactility/drivers/esp32_i2c_master.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_lcd_axs15231b.h>
|
||||
#include <esp_lcd_io_i2c.h>
|
||||
#include <esp_lcd_panel_io.h>
|
||||
#include <esp_lcd_touch.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "AXS15231BTouch"
|
||||
#define GET_CONFIG(device) (static_cast<const Axs15231bTouchConfig*>((device)->config))
|
||||
|
||||
struct Axs15231bTouchInternal {
|
||||
esp_lcd_panel_io_handle_t io_handle;
|
||||
esp_lcd_touch_handle_t touch_handle;
|
||||
};
|
||||
|
||||
static inline gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// AXS15231B always sits at a fixed I2C address (ESP_LCD_TOUCH_IO_I2C_AXS15231B_ADDRESS), unlike
|
||||
// GT911's strapping-dependent address, so no bus probing is needed here.
|
||||
static esp_err_t create_io_handle(Device* parent, esp_lcd_panel_io_handle_t* out_handle) {
|
||||
esp_lcd_panel_io_i2c_config_t io_config = ESP_LCD_TOUCH_IO_I2C_AXS15231B_CONFIG();
|
||||
|
||||
auto* parent_driver = device_get_driver(parent);
|
||||
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c")) {
|
||||
auto port = static_cast<const Esp32I2cConfig*>(parent->config)->port;
|
||||
return esp_lcd_new_panel_io_i2c_v1(port, &io_config, out_handle);
|
||||
}
|
||||
if (driver_is_compatible(parent_driver, "espressif,esp32-i2c-master")) {
|
||||
auto bus = esp32_i2c_master_get_bus_handle(parent);
|
||||
io_config.scl_speed_hz = esp32_i2c_master_get_clock_frequency(parent);
|
||||
return esp_lcd_new_panel_io_i2c_v2(bus, &io_config, out_handle);
|
||||
}
|
||||
|
||||
LOG_E(TAG, "Unsupported I2C driver");
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
static error_t start(Device* device) {
|
||||
auto* parent = device_get_parent(device);
|
||||
check(device_get_type(parent) == &I2C_CONTROLLER_TYPE);
|
||||
|
||||
const auto* config = GET_CONFIG(device);
|
||||
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(malloc(sizeof(Axs15231bTouchInternal)));
|
||||
if (internal == nullptr) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
esp_err_t ret = create_io_handle(parent, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
esp_lcd_touch_config_t touch_config = {
|
||||
.x_max = config->x_max,
|
||||
.y_max = config->y_max,
|
||||
.rst_gpio_num = pin_or_nc(config->pin_reset),
|
||||
.int_gpio_num = pin_or_nc(config->pin_interrupt),
|
||||
.levels = {
|
||||
.reset = config->reset_active_high ? 1u : 0u,
|
||||
.interrupt = config->interrupt_active_high ? 1u : 0u,
|
||||
},
|
||||
.flags = {
|
||||
.swap_xy = config->swap_xy ? 1u : 0u,
|
||||
.mirror_x = config->mirror_x ? 1u : 0u,
|
||||
.mirror_y = config->mirror_y ? 1u : 0u,
|
||||
},
|
||||
.process_coordinates = nullptr,
|
||||
.interrupt_callback = nullptr,
|
||||
.user_data = nullptr,
|
||||
.driver_data = nullptr,
|
||||
};
|
||||
|
||||
ret = esp_lcd_touch_new_i2c_axs15231b(internal->io_handle, &touch_config, &internal->touch_handle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create touch handle: %s", esp_err_to_name(ret));
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
device_set_driver_data(device, internal);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
|
||||
// esp_lcd_touch_del() only releases the touch-side resources; the panel IO handle is owned
|
||||
// separately and needs its own deletion.
|
||||
if (internal->touch_handle != nullptr) {
|
||||
if (esp_lcd_touch_del(internal->touch_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete touch handle");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->touch_handle = nullptr;
|
||||
}
|
||||
|
||||
if (internal->io_handle != nullptr) {
|
||||
if (esp_lcd_panel_io_del(internal->io_handle) != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to delete panel IO handle");
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
internal->io_handle = nullptr;
|
||||
}
|
||||
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region PointerApi
|
||||
|
||||
static error_t axs15231b_touch_enter_sleep(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_enter_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_exit_sleep(Device* device) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_exit_sleep(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_read_data(Device* device, TickType_t timeout) {
|
||||
(void)timeout; // esp_lcd_touch_read_data() has no timeout parameter
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_read_data(internal->touch_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static bool axs15231b_touch_get_touched_points(Device* device, uint16_t* x, uint16_t* y, uint16_t* strength, uint8_t* point_count, uint8_t max_point_count) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_coordinates(internal->touch_handle, x, y, strength, point_count, max_point_count);
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_set_swap_xy(Device* device, bool swap) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_set_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_get_swap_xy(Device* device, bool* swap) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_swap_xy(internal->touch_handle, swap) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_set_mirror_x(Device* device, bool mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_set_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_get_mirror_x(Device* device, bool* mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_mirror_x(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_set_mirror_y(Device* device, bool mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_set_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
static error_t axs15231b_touch_get_mirror_y(Device* device, bool* mirror) {
|
||||
auto* internal = static_cast<Axs15231bTouchInternal*>(device_get_driver_data(device));
|
||||
return esp_lcd_touch_get_mirror_y(internal->touch_handle, mirror) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
static const PointerApi axs15231b_touch_pointer_api = {
|
||||
.enter_sleep = axs15231b_touch_enter_sleep,
|
||||
.exit_sleep = axs15231b_touch_exit_sleep,
|
||||
.read_data = axs15231b_touch_read_data,
|
||||
.get_touched_points = axs15231b_touch_get_touched_points,
|
||||
.set_swap_xy = axs15231b_touch_set_swap_xy,
|
||||
.get_swap_xy = axs15231b_touch_get_swap_xy,
|
||||
.set_mirror_x = axs15231b_touch_set_mirror_x,
|
||||
.get_mirror_x = axs15231b_touch_get_mirror_x,
|
||||
.set_mirror_y = axs15231b_touch_set_mirror_y,
|
||||
.get_mirror_y = axs15231b_touch_get_mirror_y,
|
||||
};
|
||||
|
||||
Driver axs15231b_touch_driver = {
|
||||
.name = "axs15231b_touch",
|
||||
.compatible = (const char*[]) { "axs,axs15231b-touch", nullptr },
|
||||
.start_device = start,
|
||||
.stop_device = stop,
|
||||
.api = &axs15231b_touch_pointer_api,
|
||||
.device_type = &POINTER_TYPE,
|
||||
.owner = &axs15231b_module,
|
||||
.internal = nullptr
|
||||
};
|
||||
@@ -1,35 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <tactility/check.h>
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/module.h>
|
||||
|
||||
extern "C" {
|
||||
|
||||
extern Driver axs15231b_display_driver;
|
||||
extern Driver axs15231b_touch_driver;
|
||||
|
||||
static error_t start() {
|
||||
/* We crash when construct fails, because if a single driver fails to construct,
|
||||
* there is no guarantee that the previously constructed drivers can be destroyed */
|
||||
check(driver_construct_add(&axs15231b_display_driver) == ERROR_NONE);
|
||||
check(driver_construct_add(&axs15231b_touch_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
/* We crash when destruct fails, because if a single driver fails to destruct,
|
||||
* there is no guarantee that the previously destroyed drivers can be recovered */
|
||||
check(driver_remove_destruct(&axs15231b_touch_driver) == ERROR_NONE);
|
||||
check(driver_remove_destruct(&axs15231b_display_driver) == ERROR_NONE);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
Module axs15231b_module = {
|
||||
.name = "axs15231b",
|
||||
.start = start,
|
||||
.stop = stop,
|
||||
.symbols = nullptr,
|
||||
.internal = nullptr
|
||||
};
|
||||
|
||||
} // extern "C"
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/esp32_i2c.h>
|
||||
#include <tactility/drivers/esp32_i2c_master.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/log.h>
|
||||
@@ -17,6 +18,9 @@
|
||||
#include <esp_lcd_touch.h>
|
||||
#include <esp_lcd_touch_ft5x06.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "FT5x06"
|
||||
@@ -25,12 +29,44 @@
|
||||
struct Ft5x06Internal {
|
||||
esp_lcd_panel_io_handle_t io_handle;
|
||||
esp_lcd_touch_handle_t touch_handle;
|
||||
// Non-null when pin_reset is configured. Owned/pulsed by this driver instead of esp_lcd_touch
|
||||
// (see pulse_reset() below) so the reset pin can live on any GPIO_CONTROLLER, not just native
|
||||
// gpio0 - esp_lcd_touch's rst_gpio_num only accepts a native ESP32 GPIO number (it calls
|
||||
// gpio_set_level() directly), and just reading a GpioPinSpec's raw pin index and handing it
|
||||
// over as if it were one (the previous pin_or_nc() behavior below) silently toggles the wrong,
|
||||
// unrelated physical pin whenever pin_reset actually points at an I2C expander.
|
||||
GpioDescriptor* reset_descriptor;
|
||||
bool reset_active_high;
|
||||
};
|
||||
|
||||
// Only valid for pin_interrupt: esp_lcd_touch only ever reads this pin's level / attaches an ISR
|
||||
// to it, both of which esp_lcd_touch performs via ESP-IDF's native gpio_* calls, so - like
|
||||
// ili9341-module's cs/dc pins - it must be a real ESP32 GPIO. pin_reset has no such requirement and
|
||||
// must never go through this helper; see pulse_reset() instead.
|
||||
static inline gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
// See esp_lcd_ili9341-module's pulse_reset() for the full rationale; same idea, same 10ms/10ms
|
||||
// timing (matches esp_lcd_touch_ft5x06's own touch_ft5x06_reset()). esp_lcd_touch's rst_gpio_num is
|
||||
// always left at GPIO_NUM_NC (see start()), under which it just skips its own reset step entirely.
|
||||
static error_t pulse_reset(GpioDescriptor* descriptor, bool active_high) {
|
||||
if (descriptor == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
error_t error = gpio_descriptor_set_level(descriptor, active_high);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
error = gpio_descriptor_set_level(descriptor, !active_high);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// FT5x06 always sits at a fixed I2C address, unlike GT911's strapping-dependent address,
|
||||
@@ -64,8 +100,38 @@ static error_t start(Device* device) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
internal->reset_descriptor = nullptr;
|
||||
internal->reset_active_high = config->reset_active_high;
|
||||
if (config->pin_reset.gpio_controller != nullptr) {
|
||||
internal->reset_descriptor = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO);
|
||||
if (internal->reset_descriptor == nullptr) {
|
||||
LOG_E(TAG, "Failed to acquire reset GPIO descriptor");
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
if (gpio_descriptor_set_flags(internal->reset_descriptor, config->pin_reset.flags | GPIO_FLAG_DIRECTION_OUTPUT) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to configure reset pin as output");
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ret = create_io_handle(parent, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (pulse_reset(internal->reset_descriptor, internal->reset_active_high) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to pulse reset pin");
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
@@ -73,7 +139,9 @@ static error_t start(Device* device) {
|
||||
esp_lcd_touch_config_t touch_config = {
|
||||
.x_max = config->x_max,
|
||||
.y_max = config->y_max,
|
||||
.rst_gpio_num = pin_or_nc(config->pin_reset),
|
||||
// Always NC: pulse_reset() above already handled the physical pin (see its comment for
|
||||
// why); esp_lcd_touch just skips its own no-op reset step when this is NC.
|
||||
.rst_gpio_num = GPIO_NUM_NC,
|
||||
.int_gpio_num = pin_or_nc(config->pin_interrupt),
|
||||
.levels = {
|
||||
.reset = config->reset_active_high ? 1u : 0u,
|
||||
@@ -94,6 +162,9 @@ static error_t start(Device* device) {
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create touch handle: %s", esp_err_to_name(ret));
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
@@ -123,6 +194,11 @@ static error_t stop(Device* device) {
|
||||
internal->io_handle = nullptr;
|
||||
}
|
||||
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
internal->reset_descriptor = nullptr;
|
||||
}
|
||||
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/esp32_i2c.h>
|
||||
#include <tactility/drivers/esp32_i2c_master.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/i2c_controller.h>
|
||||
#include <tactility/drivers/pointer.h>
|
||||
#include <tactility/log.h>
|
||||
@@ -17,6 +18,9 @@
|
||||
#include <esp_lcd_touch.h>
|
||||
#include <esp_lcd_touch_ft6x36.h>
|
||||
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "FT6x36"
|
||||
@@ -25,12 +29,44 @@
|
||||
struct Ft6x36Internal {
|
||||
esp_lcd_panel_io_handle_t io_handle;
|
||||
esp_lcd_touch_handle_t touch_handle;
|
||||
// Non-null when pin_reset is configured. Owned/pulsed by this driver instead of esp_lcd_touch
|
||||
// (see pulse_reset() below) so the reset pin can live on any GPIO_CONTROLLER, not just native
|
||||
// gpio0 - esp_lcd_touch's rst_gpio_num only accepts a native ESP32 GPIO number (it calls
|
||||
// gpio_set_level() directly), and just reading a GpioPinSpec's raw pin index and handing it
|
||||
// over as if it were one (the previous pin_or_nc() behavior below) silently toggles the wrong,
|
||||
// unrelated physical pin whenever pin_reset actually points at an I2C expander.
|
||||
GpioDescriptor* reset_descriptor;
|
||||
bool reset_active_high;
|
||||
};
|
||||
|
||||
// Only valid for pin_interrupt: esp_lcd_touch only ever reads this pin's level / attaches an ISR
|
||||
// to it via ESP-IDF's native gpio_* calls, so - like ili9341-module's cs/dc pins - it must be a
|
||||
// real ESP32 GPIO. pin_reset has no such requirement and must never go through this helper; see
|
||||
// pulse_reset() instead.
|
||||
static inline gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
// See ili9341-module's pulse_reset() for the full rationale; same idea, same 10ms/10ms timing.
|
||||
// esp_lcd_touch's rst_gpio_num is always left at GPIO_NUM_NC (see start()), under which it just
|
||||
// skips its own reset step entirely.
|
||||
static error_t pulse_reset(GpioDescriptor* descriptor, bool active_high) {
|
||||
if (descriptor == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
error_t error = gpio_descriptor_set_level(descriptor, active_high);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
error = gpio_descriptor_set_level(descriptor, !active_high);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
// FT6x36 always sits at a fixed I2C address, unlike GT911's strapping-dependent address,
|
||||
@@ -64,8 +100,38 @@ static error_t start(Device* device) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
internal->reset_descriptor = nullptr;
|
||||
internal->reset_active_high = config->reset_active_high;
|
||||
if (config->pin_reset.gpio_controller != nullptr) {
|
||||
internal->reset_descriptor = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO);
|
||||
if (internal->reset_descriptor == nullptr) {
|
||||
LOG_E(TAG, "Failed to acquire reset GPIO descriptor");
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
if (gpio_descriptor_set_flags(internal->reset_descriptor, config->pin_reset.flags | GPIO_FLAG_DIRECTION_OUTPUT) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to configure reset pin as output");
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ret = create_io_handle(parent, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
if (pulse_reset(internal->reset_descriptor, internal->reset_active_high) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to pulse reset pin");
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
@@ -73,7 +139,9 @@ static error_t start(Device* device) {
|
||||
esp_lcd_touch_config_t touch_config = {
|
||||
.x_max = config->x_max,
|
||||
.y_max = config->y_max,
|
||||
.rst_gpio_num = pin_or_nc(config->pin_reset),
|
||||
// Always NC: pulse_reset() above already handled the physical pin (see its comment for
|
||||
// why); esp_lcd_touch just skips its own no-op reset step when this is NC.
|
||||
.rst_gpio_num = GPIO_NUM_NC,
|
||||
.int_gpio_num = pin_or_nc(config->pin_interrupt),
|
||||
.levels = {
|
||||
.reset = config->reset_active_high ? 1u : 0u,
|
||||
@@ -94,6 +162,9 @@ static error_t start(Device* device) {
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create touch handle: %s", esp_err_to_name(ret));
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
@@ -123,6 +194,11 @@ static error_t stop(Device* device) {
|
||||
internal->io_handle = nullptr;
|
||||
}
|
||||
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
internal->reset_descriptor = nullptr;
|
||||
}
|
||||
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
return ERROR_NONE;
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#define TAG "GT911"
|
||||
constexpr auto* TAG = "GT911";
|
||||
#define GET_CONFIG(device) (static_cast<const Gt911Config*>((device)->config))
|
||||
|
||||
struct Gt911Internal {
|
||||
@@ -27,7 +27,7 @@ struct Gt911Internal {
|
||||
esp_lcd_touch_handle_t touch_handle;
|
||||
};
|
||||
|
||||
static inline gpio_num_t pin_or_nc(const struct GpioPinSpec& pin) {
|
||||
static gpio_num_t pin_or_nc(const GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? GPIO_NUM_NC : static_cast<gpio_num_t>(pin.pin);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <tactility/driver.h>
|
||||
#include <tactility/drivers/display.h>
|
||||
#include <tactility/drivers/esp32_spi.h>
|
||||
#include <tactility/drivers/gpio_controller.h>
|
||||
#include <tactility/drivers/spi_controller.h>
|
||||
#include <tactility/error.h>
|
||||
#include <tactility/log.h>
|
||||
@@ -19,6 +20,7 @@
|
||||
#include <esp_lcd_ili9341.h>
|
||||
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -39,6 +41,10 @@ struct Ili9341Internal {
|
||||
// (see lvgl_display.c: the caller reuses/overwrites the color buffer as soon as draw_bitmap
|
||||
// returns) - esp_lcd_panel_draw_bitmap() itself only queues the transfer and returns early.
|
||||
SemaphoreHandle_t draw_done_semaphore;
|
||||
// Non-null when pin_reset is configured. Owned/pulsed by this driver instead of esp_lcd (see
|
||||
// pulse_reset() below) so the reset pin can live on any GPIO_CONTROLLER, not just native gpio0.
|
||||
GpioDescriptor* reset_descriptor;
|
||||
bool reset_active_high;
|
||||
};
|
||||
|
||||
// Fires for every completed SPI transaction on this IO (not just draw_bitmap's color transfers -
|
||||
@@ -50,10 +56,38 @@ static bool IRAM_ATTR on_color_trans_done(esp_lcd_panel_io_handle_t, esp_lcd_pan
|
||||
return high_task_woken == pdTRUE;
|
||||
}
|
||||
|
||||
// Only valid for cs_gpio_num/dc_gpio_num: esp_lcd_panel_io_spi toggles DC synchronously with the
|
||||
// SPI/DMA hardware itself (gpio_set_level() on the raw pin number), which is only possible for a
|
||||
// native ESP32 GPIO - it cannot be routed through an I2C GPIO expander. pin_reset has no such
|
||||
// constraint (see pulse_reset() below) and must never go through this helper.
|
||||
static int pin_or_unused(const struct GpioPinSpec& pin) {
|
||||
return pin.gpio_controller == nullptr ? -1 : static_cast<int>(pin.pin);
|
||||
}
|
||||
|
||||
// esp_lcd's reset_gpio_num has the same native-GPIO-only limitation as cs/dc (see pin_or_unused),
|
||||
// but unlike them it's just a plain static pulse, not something driven by SPI hardware timing - so
|
||||
// it's pulsed here through the generic gpio_descriptor API, which works for any GPIO_CONTROLLER
|
||||
// (native gpio0 or an I2C expander alike). esp_lcd's own reset_gpio_num is always left at -1 (see
|
||||
// start()), which makes it fall back to sending a SWRESET command instead - itself a valid and
|
||||
// commonly-recommended reset path, so this pulse plus that fallback is redundant-safe, not harmful.
|
||||
// Timing (10ms low, 10ms recovery) matches esp_lcd_ili9341's own hardware-reset path exactly.
|
||||
static error_t pulse_reset(GpioDescriptor* descriptor, bool active_high) {
|
||||
if (descriptor == nullptr) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
error_t error = gpio_descriptor_set_level(descriptor, active_high);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
error = gpio_descriptor_set_level(descriptor, !active_high);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(10));
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// region Driver lifecycle
|
||||
|
||||
static error_t start(Device* device) {
|
||||
@@ -80,6 +114,25 @@ static error_t start(Device* device) {
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
internal->reset_descriptor = nullptr;
|
||||
internal->reset_active_high = config->reset_active_high;
|
||||
if (config->pin_reset.gpio_controller != nullptr) {
|
||||
internal->reset_descriptor = gpio_descriptor_acquire(config->pin_reset.gpio_controller, config->pin_reset.pin, GPIO_OWNER_GPIO);
|
||||
if (internal->reset_descriptor == nullptr) {
|
||||
LOG_E(TAG, "Failed to acquire reset GPIO descriptor");
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
if (gpio_descriptor_set_flags(internal->reset_descriptor, config->pin_reset.flags | GPIO_FLAG_DIRECTION_OUTPUT) != ERROR_NONE) {
|
||||
LOG_E(TAG, "Failed to configure reset pin as output");
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
}
|
||||
|
||||
esp_lcd_panel_io_spi_config_t io_config = {
|
||||
.cs_gpio_num = pin_or_unused(cs_pin),
|
||||
.dc_gpio_num = pin_or_unused(config->pin_dc),
|
||||
@@ -107,13 +160,18 @@ static error_t start(Device* device) {
|
||||
esp_err_t ret = esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)spi_config->host, &io_config, &internal->io_handle);
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel IO: %s", esp_err_to_name(ret));
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
esp_lcd_panel_dev_config_t panel_config = {
|
||||
.reset_gpio_num = pin_or_unused(config->pin_reset),
|
||||
// Always -1: pulse_reset() handles the physical pin itself (see its comment for why), and
|
||||
// esp_lcd_panel_reset() below falls back to a SWRESET command when this is -1.
|
||||
.reset_gpio_num = -1,
|
||||
.rgb_ele_order = config->bgr_order ? LCD_RGB_ELEMENT_ORDER_BGR : LCD_RGB_ELEMENT_ORDER_RGB,
|
||||
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
|
||||
.bits_per_pixel = config->bits_per_pixel,
|
||||
@@ -125,6 +183,9 @@ static error_t start(Device* device) {
|
||||
if (ret != ESP_OK) {
|
||||
LOG_E(TAG, "Failed to create panel: %s", esp_err_to_name(ret));
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
@@ -134,6 +195,7 @@ static error_t start(Device* device) {
|
||||
// Every failure path below must clean up fully: unlike stop_device, this is never retried by the kernel
|
||||
// if start_device fails (see device_start() in TactilityKernel), so a partial failure here would leak.
|
||||
bool ok =
|
||||
pulse_reset(internal->reset_descriptor, internal->reset_active_high) == ERROR_NONE &&
|
||||
esp_lcd_panel_reset(internal->panel_handle) == ESP_OK &&
|
||||
esp_lcd_panel_init(internal->panel_handle) == ESP_OK &&
|
||||
(!config->invert_color || esp_lcd_panel_invert_color(internal->panel_handle, true) == ESP_OK);
|
||||
@@ -153,6 +215,9 @@ static error_t start(Device* device) {
|
||||
LOG_E(TAG, "Failed to bring up panel");
|
||||
esp_lcd_panel_del(internal->panel_handle);
|
||||
esp_lcd_panel_io_del(internal->io_handle);
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
}
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal);
|
||||
return ERROR_RESOURCE;
|
||||
@@ -181,6 +246,11 @@ static error_t stop(Device* device) {
|
||||
internal->io_handle = nullptr;
|
||||
}
|
||||
|
||||
if (internal->reset_descriptor != nullptr) {
|
||||
gpio_descriptor_release(internal->reset_descriptor);
|
||||
internal->reset_descriptor = nullptr;
|
||||
}
|
||||
|
||||
vSemaphoreDelete(internal->draw_done_semaphore);
|
||||
free(internal);
|
||||
device_set_driver_data(device, nullptr);
|
||||
@@ -193,6 +263,10 @@ static error_t stop(Device* device) {
|
||||
|
||||
static error_t ili9341_reset(Device* device) {
|
||||
auto* internal = static_cast<Ili9341Internal*>(device_get_driver_data(device));
|
||||
error_t error = pulse_reset(internal->reset_descriptor, internal->reset_active_high);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
return esp_lcd_panel_reset(internal->panel_handle) == ESP_OK ? ERROR_NONE : ERROR_RESOURCE;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user