1{ config, pkgs, lib, ... }:
2
3with lib;
4let
5 cfg = config.services.undervolt;
6
7 mkPLimit = limit: window:
8 if (limit == null && window == null) then null
9 else assert asserts.assertMsg (limit != null && window != null) "Both power limit and window must be set";
10 "${toString limit} ${toString window}";
11 cliArgs = lib.cli.toGNUCommandLine {} {
12 inherit (cfg)
13 verbose
14 temp
15 turbo
16 ;
17 # `core` and `cache` are both intentionally set to `cfg.coreOffset` as according to the undervolt docs:
18 #
19 # Core or Cache offsets have no effect. It is not possible to set different offsets for
20 # CPU Core and Cache. The CPU will take the smaller of the two offsets, and apply that to
21 # both CPU and Cache. A warning message will be displayed if you attempt to set different offsets.
22 core = cfg.coreOffset;
23 cache = cfg.coreOffset;
24 gpu = cfg.gpuOffset;
25 uncore = cfg.uncoreOffset;
26 analogio = cfg.analogioOffset;
27
28 temp-bat = cfg.tempBat;
29 temp-ac = cfg.tempAc;
30
31 power-limit-long = mkPLimit cfg.p1.limit cfg.p1.window;
32 power-limit-short = mkPLimit cfg.p2.limit cfg.p2.window;
33 };
34in
35{
36 options.services.undervolt = {
37 enable = mkEnableOption ''
38 Undervolting service for Intel CPUs.
39
40 Warning: This service is not endorsed by Intel and may permanently damage your hardware. Use at your own risk
41 '';
42
43 verbose = mkOption {
44 type = types.bool;
45 default = false;
46 description = ''
47 Whether to enable verbose logging.
48 '';
49 };
50
51 package = mkPackageOption pkgs "undervolt" { };
52
53 coreOffset = mkOption {
54 type = types.nullOr types.int;
55 default = null;
56 description = ''
57 The amount of voltage in mV to offset the CPU cores by.
58 '';
59 };
60
61 gpuOffset = mkOption {
62 type = types.nullOr types.int;
63 default = null;
64 description = ''
65 The amount of voltage in mV to offset the GPU by.
66 '';
67 };
68
69 uncoreOffset = mkOption {
70 type = types.nullOr types.int;
71 default = null;
72 description = ''
73 The amount of voltage in mV to offset uncore by.
74 '';
75 };
76
77 analogioOffset = mkOption {
78 type = types.nullOr types.int;
79 default = null;
80 description = ''
81 The amount of voltage in mV to offset analogio by.
82 '';
83 };
84
85 temp = mkOption {
86 type = types.nullOr types.int;
87 default = null;
88 description = ''
89 The temperature target in Celsius degrees.
90 '';
91 };
92
93 tempAc = mkOption {
94 type = types.nullOr types.int;
95 default = null;
96 description = ''
97 The temperature target on AC power in Celsius degrees.
98 '';
99 };
100
101 tempBat = mkOption {
102 type = types.nullOr types.int;
103 default = null;
104 description = ''
105 The temperature target on battery power in Celsius degrees.
106 '';
107 };
108
109 turbo = mkOption {
110 type = types.nullOr types.int;
111 default = null;
112 description = ''
113 Changes the Intel Turbo feature status (1 is disabled and 0 is enabled).
114 '';
115 };
116
117 p1.limit = mkOption {
118 type = with types; nullOr int;
119 default = null;
120 description = ''
121 The P1 Power Limit in Watts.
122 Both limit and window must be set.
123 '';
124 };
125 p1.window = mkOption {
126 type = with types; nullOr (oneOf [ float int ]);
127 default = null;
128 description = ''
129 The P1 Time Window in seconds.
130 Both limit and window must be set.
131 '';
132 };
133
134 p2.limit = mkOption {
135 type = with types; nullOr int;
136 default = null;
137 description = ''
138 The P2 Power Limit in Watts.
139 Both limit and window must be set.
140 '';
141 };
142 p2.window = mkOption {
143 type = with types; nullOr (oneOf [ float int ]);
144 default = null;
145 description = ''
146 The P2 Time Window in seconds.
147 Both limit and window must be set.
148 '';
149 };
150
151 useTimer = mkOption {
152 type = types.bool;
153 default = false;
154 description = ''
155 Whether to set a timer that applies the undervolt settings every 30s.
156 This will cause spam in the journal but might be required for some
157 hardware under specific conditions.
158 Enable this if your undervolt settings don't hold.
159 '';
160 };
161 };
162
163 config = mkIf cfg.enable {
164 hardware.cpu.x86.msr.enable = true;
165
166 environment.systemPackages = [ cfg.package ];
167
168 systemd.services.undervolt = {
169 description = "Intel Undervolting Service";
170
171 # Apply undervolt on boot, nixos generation switch and resume
172 wantedBy = [ "multi-user.target" "post-resume.target" ];
173 after = [ "post-resume.target" ]; # Not sure why but it won't work without this
174
175 serviceConfig = {
176 Type = "oneshot";
177 Restart = "no";
178 ExecStart = "${cfg.package}/bin/undervolt ${toString cliArgs}";
179 };
180 };
181
182 systemd.timers.undervolt = mkIf cfg.useTimer {
183 description = "Undervolt timer to ensure voltage settings are always applied";
184 partOf = [ "undervolt.service" ];
185 wantedBy = [ "multi-user.target" ];
186 timerConfig = {
187 OnBootSec = "2min";
188 OnUnitActiveSec = "30";
189 };
190 };
191 };
192}