Skip to content

Commit ab58016

Browse files
dilingerjwrdegoede
authored andcommitted
platform/x86:dell-laptop: Add knobs to change battery charge settings
The Dell BIOS allows you to set custom charging modes, which is useful in particular for extending battery life. This adds support for tweaking those various settings from Linux via sysfs knobs. One might, for example, have their laptop plugged into power at their desk the vast majority of the time and choose fairly aggressive battery-saving settings (eg, only charging once the battery drops below 50% and only charging up to 80%). When leaving for a trip, it would be more useful to instead switch to a standard charging mode (top off at 100%, charge any time power is available). Rebooting into the BIOS to change the charging mode settings is a hassle. For the Custom charging type mode, we reuse the common charge_control_{start,end}_threshold sysfs power_supply entries. The BIOS also has a bunch of other charging modes (with varying levels of usefulness), so this also adds a 'charge_type' sysfs entry that maps the standard values to Dell-specific ones. This work is based on a patch by Perry Yuan <perry_yuan@dell.com> and Limonciello Mario <Mario_Limonciello@Dell.com> submitted back in 2020. Hans de Goede: s/charge_type/charge_types/ since charge_types_show() used the new charge_types power-supply property output format. Signed-off-by: Andres Salomon <dilinger@queued.net> Reviewed-by: Pali Rohár <pali@kernel.org> Link: https://lore.kernel.org/r/20240820033005.09e03af1@5400 Reviewed-by: Hans de Goede <hdegoede@redhat.com> Signed-off-by: Hans de Goede <hdegoede@redhat.com>
1 parent c34068c commit ab58016

File tree

3 files changed

+318
-0
lines changed

3 files changed

+318
-0
lines changed

drivers/platform/x86/dell/Kconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ config DELL_LAPTOP
4949
default m
5050
depends on DMI
5151
depends on BACKLIGHT_CLASS_DEVICE
52+
depends on ACPI_BATTERY
5253
depends on ACPI_VIDEO || ACPI_VIDEO = n
5354
depends on RFKILL || RFKILL = n
5455
depends on DELL_WMI || DELL_WMI = n

drivers/platform/x86/dell/dell-laptop.c

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,13 @@
2222
#include <linux/io.h>
2323
#include <linux/rfkill.h>
2424
#include <linux/power_supply.h>
25+
#include <linux/sysfs.h>
2526
#include <linux/acpi.h>
2627
#include <linux/mm.h>
2728
#include <linux/i8042.h>
2829
#include <linux/debugfs.h>
2930
#include <linux/seq_file.h>
31+
#include <acpi/battery.h>
3032
#include <acpi/video.h>
3133
#include "dell-rbtn.h"
3234
#include "dell-smbios.h"
@@ -99,6 +101,20 @@ static bool force_rfkill;
99101
static bool micmute_led_registered;
100102
static bool mute_led_registered;
101103

104+
struct battery_mode_info {
105+
int token;
106+
const char *label;
107+
};
108+
109+
static const struct battery_mode_info battery_modes[] = {
110+
{ BAT_PRI_AC_MODE_TOKEN, "Trickle" },
111+
{ BAT_EXPRESS_MODE_TOKEN, "Fast" },
112+
{ BAT_STANDARD_MODE_TOKEN, "Standard" },
113+
{ BAT_ADAPTIVE_MODE_TOKEN, "Adaptive" },
114+
{ BAT_CUSTOM_MODE_TOKEN, "Custom" },
115+
};
116+
static u32 battery_supported_modes;
117+
102118
module_param(force_rfkill, bool, 0444);
103119
MODULE_PARM_DESC(force_rfkill, "enable rfkill on non whitelisted models");
104120

@@ -353,6 +369,32 @@ static const struct dmi_system_id dell_quirks[] __initconst = {
353369
{ }
354370
};
355371

372+
/* -1 is a sentinel value, telling us to use token->value */
373+
#define USE_TVAL ((u32) -1)
374+
static int dell_send_request_for_tokenid(struct calling_interface_buffer *buffer,
375+
u16 class, u16 select, u16 tokenid,
376+
u32 val)
377+
{
378+
struct calling_interface_token *token;
379+
380+
token = dell_smbios_find_token(tokenid);
381+
if (!token)
382+
return -ENODEV;
383+
384+
if (val == USE_TVAL)
385+
val = token->value;
386+
387+
dell_fill_request(buffer, token->location, val, 0, 0);
388+
return dell_send_request(buffer, class, select);
389+
}
390+
391+
static inline int dell_set_std_token_value(struct calling_interface_buffer *buffer,
392+
u16 tokenid, u32 value)
393+
{
394+
return dell_send_request_for_tokenid(buffer, CLASS_TOKEN_WRITE,
395+
SELECT_TOKEN_STD, tokenid, value);
396+
}
397+
356398
/*
357399
* Derived from information in smbios-wireless-ctl:
358400
*
@@ -2183,6 +2225,271 @@ static struct led_classdev mute_led_cdev = {
21832225
.default_trigger = "audio-mute",
21842226
};
21852227

2228+
static int dell_battery_set_mode(const u16 tokenid)
2229+
{
2230+
struct calling_interface_buffer buffer;
2231+
2232+
return dell_set_std_token_value(&buffer, tokenid, USE_TVAL);
2233+
}
2234+
2235+
static int dell_battery_read(const u16 tokenid)
2236+
{
2237+
struct calling_interface_buffer buffer;
2238+
int err;
2239+
2240+
err = dell_send_request_for_tokenid(&buffer, CLASS_TOKEN_READ,
2241+
SELECT_TOKEN_STD, tokenid, 0);
2242+
if (err)
2243+
return err;
2244+
2245+
if (buffer.output[1] > INT_MAX)
2246+
return -EIO;
2247+
2248+
return buffer.output[1];
2249+
}
2250+
2251+
static bool dell_battery_mode_is_active(const u16 tokenid)
2252+
{
2253+
struct calling_interface_token *token;
2254+
int ret;
2255+
2256+
ret = dell_battery_read(tokenid);
2257+
if (ret < 0)
2258+
return false;
2259+
2260+
token = dell_smbios_find_token(tokenid);
2261+
/* token's already verified by dell_battery_read() */
2262+
2263+
return token->value == (u16) ret;
2264+
}
2265+
2266+
/*
2267+
* The rules: the minimum start charging value is 50%. The maximum
2268+
* start charging value is 95%. The minimum end charging value is
2269+
* 55%. The maximum end charging value is 100%. And finally, there
2270+
* has to be at least a 5% difference between start & end values.
2271+
*/
2272+
#define CHARGE_START_MIN 50
2273+
#define CHARGE_START_MAX 95
2274+
#define CHARGE_END_MIN 55
2275+
#define CHARGE_END_MAX 100
2276+
#define CHARGE_MIN_DIFF 5
2277+
2278+
static int dell_battery_set_custom_charge_start(int start)
2279+
{
2280+
struct calling_interface_buffer buffer;
2281+
int end;
2282+
2283+
start = clamp(start, CHARGE_START_MIN, CHARGE_START_MAX);
2284+
end = dell_battery_read(BAT_CUSTOM_CHARGE_END);
2285+
if (end < 0)
2286+
return end;
2287+
if ((end - start) < CHARGE_MIN_DIFF)
2288+
start = end - CHARGE_MIN_DIFF;
2289+
2290+
return dell_set_std_token_value(&buffer, BAT_CUSTOM_CHARGE_START,
2291+
start);
2292+
}
2293+
2294+
static int dell_battery_set_custom_charge_end(int end)
2295+
{
2296+
struct calling_interface_buffer buffer;
2297+
int start;
2298+
2299+
end = clamp(end, CHARGE_END_MIN, CHARGE_END_MAX);
2300+
start = dell_battery_read(BAT_CUSTOM_CHARGE_START);
2301+
if (start < 0)
2302+
return start;
2303+
if ((end - start) < CHARGE_MIN_DIFF)
2304+
end = start + CHARGE_MIN_DIFF;
2305+
2306+
return dell_set_std_token_value(&buffer, BAT_CUSTOM_CHARGE_END, end);
2307+
}
2308+
2309+
static ssize_t charge_types_show(struct device *dev,
2310+
struct device_attribute *attr,
2311+
char *buf)
2312+
{
2313+
ssize_t count = 0;
2314+
int i;
2315+
2316+
for (i = 0; i < ARRAY_SIZE(battery_modes); i++) {
2317+
bool active;
2318+
2319+
if (!(battery_supported_modes & BIT(i)))
2320+
continue;
2321+
2322+
active = dell_battery_mode_is_active(battery_modes[i].token);
2323+
count += sysfs_emit_at(buf, count, active ? "[%s] " : "%s ",
2324+
battery_modes[i].label);
2325+
}
2326+
2327+
/* convert the last space to a newline */
2328+
if (count > 0)
2329+
count--;
2330+
count += sysfs_emit_at(buf, count, "\n");
2331+
2332+
return count;
2333+
}
2334+
2335+
static ssize_t charge_types_store(struct device *dev,
2336+
struct device_attribute *attr,
2337+
const char *buf, size_t size)
2338+
{
2339+
bool matched = false;
2340+
int err, i;
2341+
2342+
for (i = 0; i < ARRAY_SIZE(battery_modes); i++) {
2343+
if (!(battery_supported_modes & BIT(i)))
2344+
continue;
2345+
2346+
if (sysfs_streq(battery_modes[i].label, buf)) {
2347+
matched = true;
2348+
break;
2349+
}
2350+
}
2351+
if (!matched)
2352+
return -EINVAL;
2353+
2354+
err = dell_battery_set_mode(battery_modes[i].token);
2355+
if (err)
2356+
return err;
2357+
2358+
return size;
2359+
}
2360+
2361+
static ssize_t charge_control_start_threshold_show(struct device *dev,
2362+
struct device_attribute *attr,
2363+
char *buf)
2364+
{
2365+
int start;
2366+
2367+
start = dell_battery_read(BAT_CUSTOM_CHARGE_START);
2368+
if (start < 0)
2369+
return start;
2370+
2371+
if (start > CHARGE_START_MAX)
2372+
return -EIO;
2373+
2374+
return sysfs_emit(buf, "%d\n", start);
2375+
}
2376+
2377+
static ssize_t charge_control_start_threshold_store(struct device *dev,
2378+
struct device_attribute *attr,
2379+
const char *buf, size_t size)
2380+
{
2381+
int ret, start;
2382+
2383+
ret = kstrtoint(buf, 10, &start);
2384+
if (ret)
2385+
return ret;
2386+
if (start < 0 || start > 100)
2387+
return -EINVAL;
2388+
2389+
ret = dell_battery_set_custom_charge_start(start);
2390+
if (ret)
2391+
return ret;
2392+
2393+
return size;
2394+
}
2395+
2396+
static ssize_t charge_control_end_threshold_show(struct device *dev,
2397+
struct device_attribute *attr,
2398+
char *buf)
2399+
{
2400+
int end;
2401+
2402+
end = dell_battery_read(BAT_CUSTOM_CHARGE_END);
2403+
if (end < 0)
2404+
return end;
2405+
2406+
if (end > CHARGE_END_MAX)
2407+
return -EIO;
2408+
2409+
return sysfs_emit(buf, "%d\n", end);
2410+
}
2411+
2412+
static ssize_t charge_control_end_threshold_store(struct device *dev,
2413+
struct device_attribute *attr,
2414+
const char *buf, size_t size)
2415+
{
2416+
int ret, end;
2417+
2418+
ret = kstrtouint(buf, 10, &end);
2419+
if (ret)
2420+
return ret;
2421+
if (end < 0 || end > 100)
2422+
return -EINVAL;
2423+
2424+
ret = dell_battery_set_custom_charge_end(end);
2425+
if (ret)
2426+
return ret;
2427+
2428+
return size;
2429+
}
2430+
2431+
static DEVICE_ATTR_RW(charge_control_start_threshold);
2432+
static DEVICE_ATTR_RW(charge_control_end_threshold);
2433+
static DEVICE_ATTR_RW(charge_types);
2434+
2435+
static struct attribute *dell_battery_attrs[] = {
2436+
&dev_attr_charge_control_start_threshold.attr,
2437+
&dev_attr_charge_control_end_threshold.attr,
2438+
&dev_attr_charge_types.attr,
2439+
NULL,
2440+
};
2441+
ATTRIBUTE_GROUPS(dell_battery);
2442+
2443+
static int dell_battery_add(struct power_supply *battery,
2444+
struct acpi_battery_hook *hook)
2445+
{
2446+
/* this currently only supports the primary battery */
2447+
if (strcmp(battery->desc->name, "BAT0") != 0)
2448+
return -ENODEV;
2449+
2450+
return device_add_groups(&battery->dev, dell_battery_groups);
2451+
}
2452+
2453+
static int dell_battery_remove(struct power_supply *battery,
2454+
struct acpi_battery_hook *hook)
2455+
{
2456+
device_remove_groups(&battery->dev, dell_battery_groups);
2457+
return 0;
2458+
}
2459+
2460+
static struct acpi_battery_hook dell_battery_hook = {
2461+
.add_battery = dell_battery_add,
2462+
.remove_battery = dell_battery_remove,
2463+
.name = "Dell Primary Battery Extension",
2464+
};
2465+
2466+
static u32 __init battery_get_supported_modes(void)
2467+
{
2468+
u32 modes = 0;
2469+
int i;
2470+
2471+
for (i = 0; i < ARRAY_SIZE(battery_modes); i++) {
2472+
if (dell_smbios_find_token(battery_modes[i].token))
2473+
modes |= BIT(i);
2474+
}
2475+
2476+
return modes;
2477+
}
2478+
2479+
static void __init dell_battery_init(struct device *dev)
2480+
{
2481+
battery_supported_modes = battery_get_supported_modes();
2482+
2483+
if (battery_supported_modes != 0)
2484+
battery_hook_register(&dell_battery_hook);
2485+
}
2486+
2487+
static void dell_battery_exit(void)
2488+
{
2489+
if (battery_supported_modes != 0)
2490+
battery_hook_unregister(&dell_battery_hook);
2491+
}
2492+
21862493
static int __init dell_init(void)
21872494
{
21882495
struct calling_interface_token *token;
@@ -2219,6 +2526,7 @@ static int __init dell_init(void)
22192526
touchpad_led_init(&platform_device->dev);
22202527

22212528
kbd_led_init(&platform_device->dev);
2529+
dell_battery_init(&platform_device->dev);
22222530

22232531
dell_laptop_dir = debugfs_create_dir("dell_laptop", NULL);
22242532
debugfs_create_file("rfkill", 0444, dell_laptop_dir, NULL,
@@ -2293,6 +2601,7 @@ static int __init dell_init(void)
22932601
if (mute_led_registered)
22942602
led_classdev_unregister(&mute_led_cdev);
22952603
fail_led:
2604+
dell_battery_exit();
22962605
dell_cleanup_rfkill();
22972606
fail_rfkill:
22982607
platform_device_del(platform_device);
@@ -2311,6 +2620,7 @@ static void __exit dell_exit(void)
23112620
if (quirks && quirks->touchpad_led)
23122621
touchpad_led_exit();
23132622
kbd_led_exit();
2623+
dell_battery_exit();
23142624
backlight_device_unregister(dell_backlight_device);
23152625
if (micmute_led_registered)
23162626
led_classdev_unregister(&micmute_led_cdev);

drivers/platform/x86/dell/dell-smbios.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
#define KBD_LED_AUTO_50_TOKEN 0x02EB
3434
#define KBD_LED_AUTO_75_TOKEN 0x02EC
3535
#define KBD_LED_AUTO_100_TOKEN 0x02F6
36+
#define BAT_PRI_AC_MODE_TOKEN 0x0341
37+
#define BAT_ADAPTIVE_MODE_TOKEN 0x0342
38+
#define BAT_CUSTOM_MODE_TOKEN 0x0343
39+
#define BAT_STANDARD_MODE_TOKEN 0x0346
40+
#define BAT_EXPRESS_MODE_TOKEN 0x0347
41+
#define BAT_CUSTOM_CHARGE_START 0x0349
42+
#define BAT_CUSTOM_CHARGE_END 0x034A
3643
#define GLOBAL_MIC_MUTE_ENABLE 0x0364
3744
#define GLOBAL_MIC_MUTE_DISABLE 0x0365
3845
#define GLOBAL_MUTE_ENABLE 0x058C

0 commit comments

Comments
 (0)