Files
argon40-battery-display/battery/oneUpPower.c
2025-10-06 21:03:11 -04:00

449 lines
11 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* Based heavily on:
* https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tree/drivers/power/test_power.c?id=refs/tags/v4.2.6/
*/
#include <linux/fs.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kthread.h>
#include <linux/miscdevice.h>
#include <linux/module.h>
#include <linux/power_supply.h>
#include <linux/errno.h>
#include <linux/delay.h>
#include <linux/sched.h>
#include <generated/utsrelease.h>
enum test_power_id {
ONEUP_BATTERY,
ONEUP_AC,
ONEUP_POWER_NUM,
};
//
// Useful definitions. Note that the TOTAL_* definitions need to be worked out...
//
#define BLKDRV_NAME "oneUpPower"
#define TOTAL_LIFE_SECONDS (3 * 60 * 60) // Time in seconds
#define TOTAL_CHARGE (4800 * 1000) // Power in micro Amp Hours, uAH
#define TOTAL_CHARGE_FULL_SECONDS (60 * 60) // Time to full charge in seconds
//
// I2C Addresses
//
#define I2C_BUS 0x01
#define BATTERY_ADDR 0x64
#define CURRENT_HIGH_REG 0x0E
#define CURRENT_LOW_REG 0x0F
#define SOC_HIGH_REG 0x04
#define SOC_LOW_REG 0x05
//
// Needed data structures
//
struct PowerStatus {
int status; // Status of the power supply
int capacity; // Capacity in percentage
int capacity_level; // What level are we at, CRITICAL,LOW,NORMAL,HIGH,FULL
int health; // State of the battery
int present; // Is the battery present (always YES)
int technology; // What technology is the battery (LION)
int timeleft; // How much time to we have left in seconds
int temperature; // What is the battery temperature
int voltage; // What is the current voltage of the battery
} battery = {
.status = POWER_SUPPLY_STATUS_DISCHARGING,
.capacity = 90,
.capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_HIGH,
.health = POWER_SUPPLY_HEALTH_GOOD,
.present = 1,
.technology = POWER_SUPPLY_TECHNOLOGY_LION,
.timeleft = TOTAL_LIFE_SECONDS,
.temperature = 30,
.voltage = (4200 * 1000), // uV
};
//
// Forward declairations
//
static int get_battery_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val);
static int get_ac_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val );
static int ac_online = 1; // Are we connected to an external power source?
static bool module_initialized = false; // Has the driver been initialized?
static struct task_struct *monitor_task = NULL;
static enum power_supply_property power_ac_props[] = {
POWER_SUPPLY_PROP_ONLINE,
};
static enum power_supply_property power_battery_props[] = {
POWER_SUPPLY_PROP_STATUS,
POWER_SUPPLY_PROP_CHARGE_TYPE,
POWER_SUPPLY_PROP_HEALTH,
POWER_SUPPLY_PROP_PRESENT,
POWER_SUPPLY_PROP_TECHNOLOGY,
POWER_SUPPLY_PROP_CHARGE_EMPTY,
POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN,
POWER_SUPPLY_PROP_CHARGE_FULL,
POWER_SUPPLY_PROP_CHARGE_NOW,
POWER_SUPPLY_PROP_CAPACITY,
POWER_SUPPLY_PROP_CAPACITY_LEVEL,
POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG,
POWER_SUPPLY_PROP_TIME_TO_FULL_NOW,
POWER_SUPPLY_PROP_MODEL_NAME,
POWER_SUPPLY_PROP_MANUFACTURER,
POWER_SUPPLY_PROP_SERIAL_NUMBER,
POWER_SUPPLY_PROP_TEMP,
POWER_SUPPLY_PROP_VOLTAGE_NOW,
};
static char *ac_power_supplied_to[] = {
"BAT0",
};
static struct power_supply *power_supplies[ONEUP_POWER_NUM];
static const struct power_supply_desc power_descriptions[] = {
[ONEUP_BATTERY] = {
.name = "BAT0",
.type = POWER_SUPPLY_TYPE_BATTERY,
.properties = power_battery_props,
.num_properties = ARRAY_SIZE(power_battery_props),
.get_property = get_battery_property,
},
[ONEUP_AC] = {
.name = "AC0",
.type = POWER_SUPPLY_TYPE_MAINS,
.properties = power_ac_props,
.num_properties = ARRAY_SIZE(power_ac_props),
.get_property = get_ac_property,
},
};
static const struct power_supply_config power_configs[] = {
{ /* battery */
},
{
/* ac */
.supplied_to = ac_power_supplied_to,
.num_supplicants = ARRAY_SIZE(ac_power_supplied_to),
},
};
/*
* set_power_states
*
* Given the current state of the capacity and status of the AC plug,
* make sure we normalize the data associated with those levels.
*
*/
static void set_power_states( void )
{
int capacity = battery.capacity;
if( capacity > 95 ){
battery.capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_FULL;
}
else if( capacity > 85 ){
battery.capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_HIGH;
}
else if( capacity > 40 ){
battery.capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL;
}
else if( capacity > 30 ){
battery.capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_LOW;
}
else {
battery.capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL;
}
if( ac_online ){
if( capacity > 95 ){
battery.status = POWER_SUPPLY_STATUS_FULL;
}
else {
battery.status = POWER_SUPPLY_STATUS_CHARGING;
}
}
else {
battery.status = POWER_SUPPLY_STATUS_DISCHARGING;
}
}
static void check_ac_power( struct i2c_client *client )
{
int current_high;
int plugged_in;
current_high = i2c_smbus_read_byte_data( client, CURRENT_HIGH_REG );
if( (current_high & 0x80) > 0 ){
plugged_in = 0;
}
else{
plugged_in = 1;
}
if( ac_online != plugged_in ){
ac_online = plugged_in;
set_power_states();
//power_supply_changed( power_supplies[ONEUP_AC] );
}
}
static void check_battery_state( struct i2c_client *client )
{
int SOCPercent;
SOCPercent = i2c_smbus_read_byte_data( client, SOC_HIGH_REG );
if( SOCPercent > 100 )
SOCPercent = 100;
if( SOCPercent < 0 )
SOCPercent = 0;
if( battery.capacity != SOCPercent ){
battery.capacity = SOCPercent;
set_power_states();
power_supply_changed( power_supplies[ONEUP_BATTERY] );
}
}
static int system_monitor( void *args )
{
struct i2c_client *client = NULL;
struct i2c_adapter *adapter = NULL;
struct i2c_board_info board_info = {I2C_BOARD_INFO("argon40_battery", BATTERY_ADDR )};
pr_info( "Starting system monitor...\n" );
//
// Get an adapter so we can make an i2c client...
//
adapter = i2c_get_adapter( I2C_BUS );
if( adapter == NULL ){
pr_err( "Unable to get i2c adapter!\n" );
return -1;
}
pr_info( "Created an I2C adapter...\n" );
//
// Build the i2c client...
//
client = i2c_new_client_device( adapter, &board_info );
if( client == NULL ){
pr_err( "Unable to create i2c client!\n" );
return -1;
}
pr_info( "Created an I2C client device...\n" );
//
// Monitor until we are done...
//
while( true ){
set_current_state( TASK_UNINTERRUPTIBLE );
if( kthread_should_stop() )
break;
check_ac_power( client );
check_battery_state( client );
set_current_state( TASK_INTERRUPTIBLE );
schedule_timeout( HZ );
}
if( client )
{
i2c_unregister_device( client );
client = NULL;
}
if( adapter )
{
i2c_put_adapter( adapter );
adapter = NULL;
}
pr_info( "system monitor is stopping...\n" );
return 0;
}
static int get_ac_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
switch (psp) {
case POWER_SUPPLY_PROP_ONLINE:
val->intval = ac_online;
break;
default:
return -EINVAL;
}
return 0;
}
static int get_battery_int_property( struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val )
{
switch( psp ) {
case POWER_SUPPLY_PROP_STATUS:
val->intval = battery.status;
break;
case POWER_SUPPLY_PROP_CHARGE_TYPE:
val->intval = POWER_SUPPLY_CHARGE_TYPE_FAST;
break;
case POWER_SUPPLY_PROP_HEALTH:
val->intval = battery.health;
break;
case POWER_SUPPLY_PROP_PRESENT:
val->intval = battery.present;
break;
case POWER_SUPPLY_PROP_TECHNOLOGY:
val->intval = battery.technology;
break;
case POWER_SUPPLY_PROP_CAPACITY_LEVEL:
val->intval = battery.capacity_level;
break;
case POWER_SUPPLY_PROP_CAPACITY:
val->intval = battery.capacity;
break;
case POWER_SUPPLY_PROP_CHARGE_EMPTY:
val->intval = 0;
break;
case POWER_SUPPLY_PROP_CHARGE_NOW:
val->intval = battery.capacity * TOTAL_CHARGE / 100;
break;
case POWER_SUPPLY_PROP_CHARGE_FULL_DESIGN:
case POWER_SUPPLY_PROP_CHARGE_FULL:
val->intval = TOTAL_CHARGE;
break;
case POWER_SUPPLY_PROP_TIME_TO_EMPTY_AVG:
val->intval = battery.timeleft;
break;
case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW:
val->intval = (100 - battery.capacity) * TOTAL_CHARGE_FULL_SECONDS / 100;
break;
case POWER_SUPPLY_PROP_TEMP:
val->intval = battery.temperature;
break;
case POWER_SUPPLY_PROP_VOLTAGE_NOW:
val->intval = battery.voltage;
break;
default:
pr_info("%s: some properties deliberately report errors.\n",
__func__);
return -EINVAL;
}
return 0;
}
static int get_battery_property(struct power_supply *psy,
enum power_supply_property psp,
union power_supply_propval *val)
{
switch (psp) {
case POWER_SUPPLY_PROP_MODEL_NAME:
val->strval = "oneUp Battery";
break;
case POWER_SUPPLY_PROP_MANUFACTURER:
val->strval = "Argon40";
break;
case POWER_SUPPLY_PROP_SERIAL_NUMBER:
val->strval = UTS_RELEASE;
break;
default:
return get_battery_int_property( psy, psp, val );
}
return 0;
}
static int __init oneup_power_init(void)
{
int i;
int ret;
pr_info( "Starting Power monitor..." );
BUILD_BUG_ON(ONEUP_POWER_NUM != ARRAY_SIZE(power_supplies));
BUILD_BUG_ON(ONEUP_POWER_NUM != ARRAY_SIZE(power_configs));
for (i = 0; i < ARRAY_SIZE(power_supplies); i++) {
power_supplies[i] = power_supply_register(NULL,
&power_descriptions[i],
&power_configs[i]);
if (IS_ERR(power_supplies[i])) {
pr_err("%s: failed to register %s\n", __func__,
power_descriptions[i].name);
ret = PTR_ERR(power_supplies[i]);
goto failed;
}
}
monitor_task = kthread_run( system_monitor, NULL, "argon40_monitor" );
if( monitor_task == NULL ){
pr_err( "Could not start system_monitor, terminating.\n" );
goto failed;
}
set_power_states();
module_initialized = true;
return 0;
failed:
if( monitor_task ){
kthread_stop( monitor_task );
monitor_task = NULL;
}
while (--i >= 0)
power_supply_unregister(power_supplies[i]);
return ret;
}
module_init(oneup_power_init);
static void __exit oneup_power_exit(void)
{
int i;
//
// First up, stop the monitor task as its using resources
//
if( monitor_task ){
kthread_stop( monitor_task );
monitor_task = NULL;
}
/* Let's see how we handle changes... */
ac_online = 0;
battery.status = POWER_SUPPLY_STATUS_DISCHARGING;
for (i = 0; i < ARRAY_SIZE(power_supplies); i++)
power_supply_changed(power_supplies[i]);
pr_info("%s: 'changed' event sent, sleeping for 10 seconds...\n", __func__);
ssleep(10);
for (i = 0; i < ARRAY_SIZE(power_supplies); i++)
power_supply_unregister(power_supplies[i]);
module_initialized = false;
}
module_exit(oneup_power_exit);
MODULE_DESCRIPTION("Power supply driver for Argon40 1UP");
MODULE_AUTHOR("Jeff Curless <jeff@thecurlesses.com>");
MODULE_LICENSE("GPL");