diff --git a/pisugar-module/.clang-format b/pisugar-module/pisugar-2/.clang-format similarity index 100% rename from pisugar-module/.clang-format rename to pisugar-module/pisugar-2/.clang-format diff --git a/pisugar-module/.gitignore b/pisugar-module/pisugar-2/.gitignore similarity index 100% rename from pisugar-module/.gitignore rename to pisugar-module/pisugar-2/.gitignore diff --git a/pisugar-module/pisugar-2/Makefile b/pisugar-module/pisugar-2/Makefile new file mode 100644 index 0000000..4f62eef --- /dev/null +++ b/pisugar-module/pisugar-2/Makefile @@ -0,0 +1,31 @@ +TARGET = pisugar_2_battery + +obj-m += pisugar_2_battery.o + +KERN_VER = $(shell uname -r) + +SOURCEDIR = $(shell pwd) +BUILDDIR = $(shell pwd)/build + +I2C_BUS ?= 0x01 +I2C_ADDR ?= 0x75 +BAT_MODEL ?= 0 + +all: + make -C /lib/modules/$(KERN_VER)/build M=$(SOURCEDIR) modules + +clean: + rm -f *.cmd *.ko *.o Module.symvers modules.order *.mod.c .pisugar_2_battery.* .modules.* .Module.symvers.* pisugar_2_battery.mod + +install: all + cp -f pisugar_2_battery.ko /lib/modules/$(KERN_VER)/kernel/drivers/power/supply + grep -q pisugar_2_battery /etc/modules || sed -i '$$a pisugar_2_battery' /etc/modules + echo "options pisugar_2_battery i2c_bus=$(I2C_BUS) i2c_addr=$(I2C_ADDR) bat_model=$(BAT_MODEL)" > /etc/modprobe.d/pisugar_2_battery.conf + depmod -a + modprobe pisugar_2_battery + +uninstall: + rmmod pisugar_2_battery || true + rm -f /lib/modules/$(KERN_VER)/kernel/drivers/power/supply/pisugar_2_battery.ko + sed -i '/pisugar_2_battery/d' /etc/modules + rm -f /etc/modprobe.d/pisugar_2_battery.conf diff --git a/pisugar-module/pisugar-2/README.md b/pisugar-module/pisugar-2/README.md new file mode 100644 index 0000000..d1e1550 --- /dev/null +++ b/pisugar-module/pisugar-2/README.md @@ -0,0 +1,93 @@ +# PiSugar Linux kernel modules + +Linux kernel modules for PiSugar 2. + +## Preparing for building RPI kernel module + +### Linux distributions with kernel symbols + +Congratulations if your PI is running a linux distribution that has `/lib/modules/$(uname -r)/build/` directory, e.g. ubuntu-server or latest pi os, you don't need to manually build RPI kernel, and that will save a lot of time. + +Install `build-essential` and `linux-headers` +```shell +sudo apt install -y build-essential linux-headers-$(uname -r) +``` + +Running 32bit OS in a 64bit machine, see [this](https://forums.raspberrypi.com/viewtopic.php?t=367669). + +### Old raspberry Pi OS + +As kernel symbols is not included in Raspberry Pi OS (no `/lib/modules/$(uname -r)/build`), so you need to compile the kernel and generate the kernel symbols by yourself. + +To build the kernel, see official doc: https://www.raspberrypi.com/documentation/computers/linux_kernel.html + +## Compile/install/uninstall kernel module + +Clone this repository, make kernel modules: +```shell +make +``` + +Install: +```shell +sudo make install +``` + +Module params: +``` +i2c_bus i2c bus, default 0x01 +i2c_addr i2c addr, default 0x75 +bat_model PiSugar2 model, 0 (for pi zero) or 1 (pro for pi 3/4) +``` + +Install with parameters: +```shell +sudo make install i2c_bus=0x01 i2c_addr=0x75 bat_model=0 +``` + +Uninstall: +```shell +sudo make uninstall +``` + +## Manually load kernel module + +Load module: +```shell +sudo insmod pisugar_2_battery.ko +# or +sudo insmod pisugar_2_battery.ko i2c_bus=1 i2c_addr=0x75 bat_model=0 +``` + +Now, it is loaded: +```shell +lsmod | grep battery +``` + +And you will see extra device files in `/sys/class/power_supply` +```shell +ls /sys/class/power_supply +``` + +Remove module: +```shell +sudo rmmod pisugar_2_battery.ko +``` + +Now, you can enable a battery monitor plugin that reads battery status from power supply subsystem (OS battery monitor plugin or a 3rd party plugin). + +If you want to load kernel module at boot time, copy it to `/lib/modules/$(uname -r)/kernel/drivers/power/supply` +```shell +sudo cp -f pisugar_2_battery.ko /lib/modules/$(uname -r)/kernel/drivers/power/supply +sudo echo pisugar_2_battery >> /etc/modules +sudo depmod -a +``` + +You may want to change module parameters: +```shell +echo "options pisugar_2_battery i2c_bus=0x01 i2c_addr=0x75 bat_model=0" | sudo tee /etc/modprobe.d/pisugar_2_battery.conf +``` + +## License + +GPL \ No newline at end of file diff --git a/pisugar-module/pisugar-2/pisugar_2_battery.c b/pisugar-module/pisugar-2/pisugar_2_battery.c new file mode 100644 index 0000000..1f88c8d --- /dev/null +++ b/pisugar-module/pisugar-2/pisugar_2_battery.c @@ -0,0 +1,499 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* 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 + */ +/* + * Fork from https://github.com/hoelzro/linux-fake-battery-module + * https://docs.kernel.org/power/power_supply_class.html + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BAT_I2C_BUS 0x01 +#define IP5209_I2C_ADDR 0x75 +#define IP5312_I2C_ADDR 0x75 + +#define TOTAL_LIFE_SECONDS (3 * 60 * 60) +#define TOTAL_CHARGE (2000 * 1000) // uAH +#define TOTAL_CHARGE_FULL_SECONDS (60 * 60) + +const float IP5209_CURVE[][] = { + {4.16, 100.0}, + {4.05, 95.0}, + {4.00, 80.0}, + {3.92, 65.0}, + {3.86, 40.0}, + {3.79, 25.5}, + {3.66, 10.0}, + {3.52, 6.5}, + {3.49, 3.2}, + {3.1, 0.0}, +}; + +const float IP5312_CURVE[][] = { + {4.10, 100.0}, + {4.05, 95.0}, + {3.90, 88.0}, + {3.80, 77.0}, + {3.70, 65.0}, + {3.62, 55.0}, + {3.58, 49.0}, + {3.49, 25.6}, + {3.32, 4.5}, + {3.1, 0.0}, +}; + +enum BAT_MODEL { + STANDARD = 0, // ip5209, for pi zero + PRO = 1, // ip5312, for pi 3/4 +} + +enum pisugar_2_bat_reg { + pisugar_2_VER = 0x00, + pisugar_2_MOD = 0x01, + pisugar_2_CTL1 = 0x02, + pisugar_2_TEMP = 0x04, + pisugar_2_CAP = 0x2A, + pisugar_2_VOL_H = 0x22, + PISGUAR_3_VOL_L = 0x23, +}; + +#define pisugar_2_VER_3 3 +#define pisugar_2_MOD_APP 0x0F + +#define pisugar_2_MSK_CTR1_USB (1 << 7) +#define pisugar_2_MSK_CTR1_CH_EN (1 << 6) + +#define BAT_HIS_LEN 30 +float bat_voltage_his[BAT_HIS_LEN] = {0}; // mV + +static short int i2c_bus = BAT_I2C_BUS; +static short int i2c_addr = IP5209_I2C_ADDR; +static short int bat_module = STANDARD; + +module_param(i2c_bus, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); +MODULE_PARM_DESC(i2c_bus, "I2C bus default 0x01"); + +module_param(i2c_addr, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); +MODULE_PARM_DESC(i2c_addr, "I2C addr default 0x75"); + +module_param(bat_module, short, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); +MODULE_PARM_DESC(bat_module, "PiSugar 2 model, 0 standard (pi zero), 1 pro (pi 3/4)"); + +static int pisugar_2_battery_get_property1(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val); + +static int pisugar_2_ac_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val); + +static struct task_struct *pisugar_2_monitor_task = NULL; + +static struct battery_status { + int status; + int capacity_level; + int capacity; // % + int time_left; // seconds + int voltage; // uV + int temperature; +} pisugar_2_battery_statuses[1] = {{.status = POWER_SUPPLY_STATUS_FULL, + .capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_FULL, + .capacity = 100, + .time_left = TOTAL_LIFE_SECONDS, + .voltage = 4200 * 1000, // uV + .temperature = 30}}; + +static int ac_status = 1; + +static char *pisugar_2_ac_supplies[] = { + "BAT0", +}; + +static enum power_supply_property pisugar_2_battery_properties[] = { + 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 enum power_supply_property pisugar_2_ac_properties[] = { + POWER_SUPPLY_PROP_ONLINE, +}; + +static struct power_supply_desc descriptions[] = { + { + .name = "BAT0", + .type = POWER_SUPPLY_TYPE_BATTERY, + .properties = pisugar_2_battery_properties, + .num_properties = ARRAY_SIZE(pisugar_2_battery_properties), + .get_property = pisugar_2_battery_get_property1, + }, + + { + .name = "AC0", + .type = POWER_SUPPLY_TYPE_MAINS, + .properties = pisugar_2_ac_properties, + .num_properties = ARRAY_SIZE(pisugar_2_ac_properties), + .get_property = pisugar_2_ac_get_property, + }, +}; + +static struct power_supply_config configs[] = { + {}, + {}, + { + .supplied_to = pisugar_2_ac_supplies, + .num_supplicants = ARRAY_SIZE(pisugar_2_ac_supplies), + }, +}; + +static struct power_supply *supplies[sizeof(descriptions) / sizeof(descriptions[0])]; + +#define prefixed(s, prefix) (!strncmp((s), (prefix), sizeof(prefix) - 1)) + +static int pisugar_2_battery_generic_get_property(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val, + struct battery_status *status) +{ + switch (psp) { + case POWER_SUPPLY_PROP_MANUFACTURER: + val->strval = "PiSugar"; + break; + case POWER_SUPPLY_PROP_STATUS: + val->intval = status->status; + break; + case POWER_SUPPLY_PROP_CHARGE_TYPE: + val->intval = POWER_SUPPLY_CHARGE_TYPE_STANDARD; + break; + case POWER_SUPPLY_PROP_HEALTH: + val->intval = POWER_SUPPLY_HEALTH_GOOD; + break; + case POWER_SUPPLY_PROP_PRESENT: + val->intval = 1; + break; + case POWER_SUPPLY_PROP_TECHNOLOGY: + val->intval = POWER_SUPPLY_TECHNOLOGY_LION; + break; + case POWER_SUPPLY_PROP_CAPACITY: + val->intval = status->capacity; + break; + case POWER_SUPPLY_PROP_CAPACITY_LEVEL: + val->intval = status->capacity_level; + break; + case POWER_SUPPLY_PROP_CHARGE_EMPTY: + val->intval = 0; + break; + case POWER_SUPPLY_PROP_CHARGE_NOW: + val->intval = status->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 = status->time_left; + break; + case POWER_SUPPLY_PROP_TIME_TO_FULL_NOW: + val->intval = (100 - status->capacity) * TOTAL_CHARGE_FULL_SECONDS / 100; + break; + case POWER_SUPPLY_PROP_TEMP: + val->intval = status->temperature; + break; + case POWER_SUPPLY_PROP_VOLTAGE_NOW: + val->intval = status->voltage; + break; + default: + pr_info("%s: some properties deliberately report errors.\n", __func__); + return -EINVAL; + } + return 0; +}; + +static int pisugar_2_battery_get_property1(struct power_supply *psy, + enum power_supply_property psp, + union power_supply_propval *val) +{ + switch (psp) { + case POWER_SUPPLY_PROP_MODEL_NAME: + val->strval = "PiSugar battery 0"; + break; + case POWER_SUPPLY_PROP_SERIAL_NUMBER: + val->strval = ""; + break; + default: + return pisugar_2_battery_generic_get_property(psy, psp, val, &pisugar_2_battery_statuses[0]); + } + return 0; +} + +static int pisugar_2_ac_get_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_status; + break; + default: + return -EINVAL; + } + return 0; +} + +#define CHECK_VALID(val) ((val) >= 0 && (val) <= 255) + +static void push_bat_voltage(float vol) +{ + for (int i = 0; i < BAT_HIS_LEN - 1; i++) { + bat_voltage_his[i] = bat_voltage_his[i + 1]; + } + bat_voltage_his[BAT_HIS_LEN - 1] = vol; +} + +static float get_bat_avg_voltage() +{ + double vol_sum = 0; + for (int i = 0; i < BAT_HIS_LEN; i++) { + vol_sum += bat_voltage_his[i]; + } + return (float)(vol_sum / BAT_HIS_LEN); +} + +static void update_bat_capacity_level_and_status() +{ + // capacity level + int cap = pisugar_2_battery_statuses->capacity; + if (cap > 95) { + pisugar_2_battery_statuses->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_FULL; + } else if (cap > 85) { + pisugar_2_battery_statuses->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_HIGH; + } else if (cap > 40) { + pisugar_2_battery_statuses->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_NORMAL; + } else if (cap > 30) { + pisugar_2_battery_statuses->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_LOW; + } else { + pisugar_2_battery_statuses->capacity_level = POWER_SUPPLY_CAPACITY_LEVEL_CRITICAL; + } + + // bat status + if (ac_status) { + if (cap > 95) { + pisugar_2_battery_statuses->status = POWER_SUPPLY_STATUS_FULL; + } else { + pisugar_2_battery_statuses->status = POWER_SUPPLY_STATUS_CHARGING; + } + } else { + pisugar_2_battery_statuses->status = POWER_SUPPLY_STATUS_DISCHARGING; + } + +} + +static void ip5209_monitor_once(struct i2c_client *pisugar_2_client) +{ + // read voltage + int vol_low = i2c_smbus_read_byte_data(pisugar_2_client, 0xa2); + int vol_high = i2c_smbus_read_byte_data(pisugar_2_client, 0xa3); + float vol = 0; // mv + if (!CHECK_VALID(vol_high) || !CHECK_VALID(vol_low)) { + return; + } + if ((vol_high & 0x20) == 0x20) { + vol = 2600.0 - (float)((~vol_low) + (~(vol_high & 0x1F)) * 256 + 1) * 0.26855; + } else { + vol = 2600.0 + (float)(vol_low + vol_high * 256) * 0.26855; + } + push_bat_voltage(vol); + float vol_avg = get_bat_avg_voltage(); // mV + pisugar_2_battery_statuses->voltage = vol_avg * 1000; // uV + + // capacity + int cap = 0; + float vol_avg_v = vol_avg / 1000; + for (int i=0; i= IP5209_CURVE[i][0]) { + cap = IP5209_CURVE[i][1]; + } + if (i > 0) { + float vol_diff_v = vol_avg_v - IP5209_CURVE[i]; + float k = (IP5209_CURVE[i-1][1] - IP5209_CURVE[i][1]) / (IP5209_CURVE[i-1][0] - IP5209_CURVE[i][0]); + cap += (int)(k * vol_diff_v); + } + } + pisugar_2_battery_statuses->capacity = cap; + + // charging status + int charging_flags = i2c_smbus_read_byte_data(pisugar_2_client, 0x71); + ac_status = charging_flags > 0 ? 1: 0; + + update_bat_capacity_level_and_status(); +} + +static void ip5312_monitor_once() { + // read voltage + int vol_low = i2c_smbus_read_byte_data(pisugar_2_client, 0xd0); + int vol_high = i2c_smbus_read_byte_data(pisugar_2_client, 0xd1); + float vol = 0; // mv + if (!CHECK_VALID(vol_high) || !CHECK_VALID(vol_low)) { + return; + } + vol = 2600.0 + (float)(vol_low + (vol_high & (0x1F)) * 256) * 0.26855;} + push_bat_voltage(vol); + float vol_avg = get_bat_avg_voltage(); // mV + pisugar_2_battery_statuses->voltage = vol_avg * 1000; // uV + + // capacity + int cap = 0; + float vol_avg_v = vol_avg / 1000; + for (int i=0; i= IP5312_CURVE[i][0]) { + cap = IP5312_CURVE[i][1]; + } + if (i > 0) { + float vol_diff_v = vol_avg_v - IP5312_CURVE[i]; + float k = (IP5312_CURVE[i-1][1] - IP5312_CURVE[i][1]) / (IP5312_CURVE[i-1][0] - IP5312_CURVE[i][0]); + cap += (int)(k * vol_diff_v); + } + } + pisugar_2_battery_statuses->capacity = cap; + + // charging status + int charging_flags = i2c_smbus_read_byte_data(pisugar_2_client, 0x79); + ac_status = (charging_flags >> 6) > 0 ? 1: 0; + + update_bat_capacity_level_and_status(); +} + +static int pisugar_2_monitor(void *args) +{ + struct i2c_client *pisugar_2_client = NULL; + struct i2c_adapter *adapter = NULL; + struct i2c_board_info board_info = {I2C_BOARD_INFO("pisugar_2_battery", i2c_addr)}; + + // create i2c pisugar_2_client + adapter = i2c_get_adapter(i2c_bus); + if (adapter == NULL) { + printk(KERN_ERR "Unable to get i2c adapter!"); + return -1; + } + pisugar_2_client = i2c_new_client_device(adapter, &board_info); + if (pisugar_2_client == NULL) { + printk(KERN_ERR "Unable to create i2c client!"); + return -1; + } + + while (true) { + set_current_state(TASK_UNINTERRUPTIBLE); + if (kthread_should_stop()) break; + + if (bat_module == STANDARD) { + ip5209_monitor_once(pisugar_2_client); + } + if (bat_module == PRO) { + ip5312_monitor_once(pisugar_2_client); + } + + sleep: + set_current_state(TASK_RUNNING); + schedule_timeout(HZ); + } + + i2c_unregister_device(pisugar_2_client); + pisugar_2_client = NULL; + + return 0; +} + +static int __init pisugar_2_battery_init(void) +{ + int result; + int i; + + // create a monitor kthread + pisugar_2_monitor_task = kthread_run(pisugar_2_monitor, NULL, "pisugar_2_monitor"); + if (pisugar_2_monitor_task == NULL) { + goto error; + } + + // register power supply + for (i = 0; i < ARRAY_SIZE(descriptions); i++) { + supplies[i] = power_supply_register(NULL, &descriptions[i], &configs[i]); + if (IS_ERR(supplies[i])) { + printk(KERN_ERR "Unable to register power supply %d in pisugar_2_battery\n", i); + goto error; + } + } + + printk(KERN_INFO "loaded pisugar_2_battery module\n"); + return 0; + +error: + if (pisugar_2_monitor_task) { + kthread_stop(pisugar_2_monitor_task); + pisugar_2_monitor_task = NULL; + } + while (--i >= 0) { + power_supply_unregister(supplies[i]); + } + return -1; +} + +static void __exit pisugar_2_battery_exit(void) +{ + int i; + + if (pisugar_2_monitor_task) { + kthread_stop(pisugar_2_monitor_task); + pisugar_2_monitor_task = NULL; + } + + for (i = ARRAY_SIZE(descriptions) - 1; i >= 0; i--) { + power_supply_unregister(supplies[i]); + } + + printk(KERN_INFO "unloaded pisugar_2_battery module\n"); +} + +module_init(pisugar_2_battery_init); +module_exit(pisugar_2_battery_exit); + +MODULE_AUTHOR("The PiSugar Team "); +MODULE_DESCRIPTION("PiSugar 2 battery driver"); +MODULE_LICENSE("GPL"); diff --git a/pisugar-module/pisugar-3/.clang-format b/pisugar-module/pisugar-3/.clang-format new file mode 100644 index 0000000..1c3d514 --- /dev/null +++ b/pisugar-module/pisugar-3/.clang-format @@ -0,0 +1,26 @@ +BasedOnStyle: Google +IndentWidth: 4 +--- +Language: Cpp +AllowShortBlocksOnASingleLine: Never +ColumnLimit: 120 +AlignTrailingComments: true +AllowShortFunctionsOnASingleLine: Empty +AlignConsecutiveMacros: true +MaxEmptyLinesToKeep: 1 +UseTab: Never +SortIncludes: true +IncludeBlocks: Regroup +IncludeCategories: + - Regex: "^<.*>" + Priority: -99 + - Regex: ".*" + Priority: 0 +ReflowComments: true +TabWidth: 4 +BreakBeforeBraces: Custom +BraceWrapping: + AfterFunction: true +BinPackArguments: false +BinPackParameters: false +AlignEscapedNewlinesLeft: false \ No newline at end of file diff --git a/pisugar-module/pisugar-3/.gitignore b/pisugar-module/pisugar-3/.gitignore new file mode 100644 index 0000000..d163863 --- /dev/null +++ b/pisugar-module/pisugar-3/.gitignore @@ -0,0 +1 @@ +build/ \ No newline at end of file diff --git a/pisugar-module/Makefile b/pisugar-module/pisugar-3/Makefile similarity index 100% rename from pisugar-module/Makefile rename to pisugar-module/pisugar-3/Makefile diff --git a/pisugar-module/README.md b/pisugar-module/pisugar-3/README.md similarity index 100% rename from pisugar-module/README.md rename to pisugar-module/pisugar-3/README.md diff --git a/pisugar-module/pisugar_3_battery.c b/pisugar-module/pisugar-3/pisugar_3_battery.c similarity index 100% rename from pisugar-module/pisugar_3_battery.c rename to pisugar-module/pisugar-3/pisugar_3_battery.c