1{ config, pkgs, lib, ... }:
2
3with lib;
4let
5 cfg = config.services.undervolt;
6
7 mkPLimit = limit: window:
8 if (isNull limit && isNull window) then null
9 else assert asserts.assertMsg (!isNull limit && !isNull window) "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 ;
16 # `core` and `cache` are both intentionally set to `cfg.coreOffset` as according to the undervolt docs:
17 #
18 # Core or Cache offsets have no effect. It is not possible to set different offsets for
19 # CPU Core and Cache. The CPU will take the smaller of the two offsets, and apply that to
20 # both CPU and Cache. A warning message will be displayed if you attempt to set different offsets.
21 core = cfg.coreOffset;
22 cache = cfg.coreOffset;
23 gpu = cfg.gpuOffset;
24 uncore = cfg.uncoreOffset;
25 analogio = cfg.analogioOffset;
26
27 temp-bat = cfg.tempBat;
28 temp-ac = cfg.tempAc;
29
30 power-limit-long = mkPLimit cfg.p1.limit cfg.p1.window;
31 power-limit-short = mkPLimit cfg.p2.limit cfg.p2.window;
32 };
33in
34{
35 options.services.undervolt = {
36 enable = mkEnableOption ''
37 Undervolting service for Intel CPUs.
38
39 Warning: This service is not endorsed by Intel and may permanently damage your hardware. Use at your own risk!
40 '';
41
42 verbose = mkOption {
43 type = types.bool;
44 default = false;
45 description = ''
46 Whether to enable verbose logging.
47 '';
48 };
49
50 package = mkOption {
51 type = types.package;
52 default = pkgs.undervolt;
53 defaultText = "pkgs.undervolt";
54 description = ''
55 undervolt derivation to use.
56 '';
57 };
58
59 coreOffset = mkOption {
60 type = types.nullOr types.int;
61 default = null;
62 description = ''
63 The amount of voltage in mV to offset the CPU cores by.
64 '';
65 };
66
67 gpuOffset = mkOption {
68 type = types.nullOr types.int;
69 default = null;
70 description = ''
71 The amount of voltage in mV to offset the GPU by.
72 '';
73 };
74
75 uncoreOffset = mkOption {
76 type = types.nullOr types.int;
77 default = null;
78 description = ''
79 The amount of voltage in mV to offset uncore by.
80 '';
81 };
82
83 analogioOffset = mkOption {
84 type = types.nullOr types.int;
85 default = null;
86 description = ''
87 The amount of voltage in mV to offset analogio by.
88 '';
89 };
90
91 temp = mkOption {
92 type = types.nullOr types.int;
93 default = null;
94 description = ''
95 The temperature target in Celsius degrees.
96 '';
97 };
98
99 tempAc = mkOption {
100 type = types.nullOr types.int;
101 default = null;
102 description = ''
103 The temperature target on AC power in Celsius degrees.
104 '';
105 };
106
107 tempBat = mkOption {
108 type = types.nullOr types.int;
109 default = null;
110 description = ''
111 The temperature target on battery power in Celsius degrees.
112 '';
113 };
114
115 p1.limit = mkOption {
116 type = with types; nullOr int;
117 default = null;
118 description = ''
119 The P1 Power Limit in Watts.
120 Both limit and window must be set.
121 '';
122 };
123 p1.window = mkOption {
124 type = with types; nullOr (oneOf [ float int ]);
125 default = null;
126 description = ''
127 The P1 Time Window in seconds.
128 Both limit and window must be set.
129 '';
130 };
131
132 p2.limit = mkOption {
133 type = with types; nullOr int;
134 default = null;
135 description = ''
136 The P2 Power Limit in Watts.
137 Both limit and window must be set.
138 '';
139 };
140 p2.window = mkOption {
141 type = with types; nullOr (oneOf [ float int ]);
142 default = null;
143 description = ''
144 The P2 Time Window in seconds.
145 Both limit and window must be set.
146 '';
147 };
148
149 useTimer = mkOption {
150 type = types.bool;
151 default = false;
152 description = ''
153 Whether to set a timer that applies the undervolt settings every 30s.
154 This will cause spam in the journal but might be required for some
155 hardware under specific conditions.
156 Enable this if your undervolt settings don't hold.
157 '';
158 };
159 };
160
161 config = mkIf cfg.enable {
162 boot.kernelModules = [ "msr" ];
163
164 environment.systemPackages = [ cfg.package ];
165
166 systemd.services.undervolt = {
167 path = [ pkgs.undervolt ];
168
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 = "${pkgs.undervolt}/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}