Commit | Line | Data |
---|---|---|
2f34ce81 TG |
1 | /* |
2 | * OMAP3/OMAP4 Voltage Management Routines | |
3 | * | |
4 | * Author: Thara Gopinath <thara@ti.com> | |
5 | * | |
6 | * Copyright (C) 2007 Texas Instruments, Inc. | |
7 | * Rajendra Nayak <rnayak@ti.com> | |
8 | * Lesly A M <x0080970@ti.com> | |
9 | * | |
c0718df4 | 10 | * Copyright (C) 2008, 2011 Nokia Corporation |
2f34ce81 | 11 | * Kalle Jokiniemi |
c0718df4 | 12 | * Paul Walmsley |
2f34ce81 TG |
13 | * |
14 | * Copyright (C) 2010 Texas Instruments, Inc. | |
15 | * Thara Gopinath <thara@ti.com> | |
16 | * | |
17 | * This program is free software; you can redistribute it and/or modify | |
18 | * it under the terms of the GNU General Public License version 2 as | |
19 | * published by the Free Software Foundation. | |
20 | */ | |
21 | ||
22 | #include <linux/delay.h> | |
23 | #include <linux/io.h> | |
24 | #include <linux/clk.h> | |
25 | #include <linux/err.h> | |
26 | #include <linux/debugfs.h> | |
27 | #include <linux/slab.h> | |
28 | ||
29 | #include <plat/common.h> | |
2f34ce81 TG |
30 | |
31 | #include "prm-regbits-34xx.h" | |
bd38107b TG |
32 | #include "prm-regbits-44xx.h" |
33 | #include "prm44xx.h" | |
34 | #include "prcm44xx.h" | |
35 | #include "prminst44xx.h" | |
2f34ce81 TG |
36 | #include "control.h" |
37 | ||
e1d6f472 | 38 | #include "voltage.h" |
e69c22b1 | 39 | #include "powerdomain.h" |
e1d6f472 | 40 | |
c0718df4 PW |
41 | #include "vc.h" |
42 | #include "vp.h" | |
43 | ||
81a60482 | 44 | static LIST_HEAD(voltdm_list); |
2f34ce81 | 45 | |
81a60482 | 46 | #define VOLTAGE_DIR_SIZE 16 |
2f34ce81 TG |
47 | static struct dentry *voltage_dir; |
48 | ||
49 | /* Init function pointers */ | |
81a60482 | 50 | static int vp_forceupdate_scale_voltage(struct voltagedomain *voltdm, |
c0718df4 | 51 | unsigned long target_volt); |
2f34ce81 TG |
52 | |
53 | static u32 omap3_voltage_read_reg(u16 mod, u8 offset) | |
54 | { | |
55 | return omap2_prm_read_mod_reg(mod, offset); | |
56 | } | |
57 | ||
58 | static void omap3_voltage_write_reg(u32 val, u16 mod, u8 offset) | |
59 | { | |
60 | omap2_prm_write_mod_reg(val, mod, offset); | |
61 | } | |
62 | ||
bd38107b TG |
63 | static u32 omap4_voltage_read_reg(u16 mod, u8 offset) |
64 | { | |
65 | return omap4_prminst_read_inst_reg(OMAP4430_PRM_PARTITION, | |
66 | mod, offset); | |
67 | } | |
68 | ||
69 | static void omap4_voltage_write_reg(u32 val, u16 mod, u8 offset) | |
70 | { | |
71 | omap4_prminst_write_inst_reg(val, OMAP4430_PRM_PARTITION, mod, offset); | |
72 | } | |
73 | ||
81a60482 | 74 | static int __init _config_common_vdd_data(struct voltagedomain *voltdm) |
c0718df4 PW |
75 | { |
76 | char *sys_ck_name; | |
77 | struct clk *sys_ck; | |
78 | u32 sys_clk_speed, timeout_val, waittime; | |
81a60482 | 79 | struct omap_vdd_info *vdd = voltdm->vdd; |
c0718df4 PW |
80 | |
81 | /* | |
82 | * XXX Clockfw should handle this, or this should be in a | |
83 | * struct record | |
84 | */ | |
85 | if (cpu_is_omap24xx() || cpu_is_omap34xx()) | |
86 | sys_ck_name = "sys_ck"; | |
87 | else if (cpu_is_omap44xx()) | |
88 | sys_ck_name = "sys_clkin_ck"; | |
89 | else | |
90 | return -EINVAL; | |
91 | ||
92 | /* | |
93 | * Sys clk rate is require to calculate vp timeout value and | |
94 | * smpswaittimemin and smpswaittimemax. | |
95 | */ | |
96 | sys_ck = clk_get(NULL, sys_ck_name); | |
97 | if (IS_ERR(sys_ck)) { | |
98 | pr_warning("%s: Could not get the sys clk to calculate" | |
81a60482 | 99 | "various vdd_%s params\n", __func__, voltdm->name); |
c0718df4 PW |
100 | return -EINVAL; |
101 | } | |
102 | sys_clk_speed = clk_get_rate(sys_ck); | |
103 | clk_put(sys_ck); | |
104 | /* Divide to avoid overflow */ | |
105 | sys_clk_speed /= 1000; | |
106 | ||
107 | /* Generic voltage parameters */ | |
c0718df4 PW |
108 | vdd->volt_scale = vp_forceupdate_scale_voltage; |
109 | vdd->vp_enabled = false; | |
110 | ||
111 | vdd->vp_rt_data.vpconfig_erroroffset = | |
112 | (vdd->pmic_info->vp_erroroffset << | |
113 | vdd->vp_data->vp_common->vpconfig_erroroffset_shift); | |
114 | ||
115 | timeout_val = (sys_clk_speed * vdd->pmic_info->vp_timeout_us) / 1000; | |
116 | vdd->vp_rt_data.vlimitto_timeout = timeout_val; | |
117 | vdd->vp_rt_data.vlimitto_vddmin = vdd->pmic_info->vp_vddmin; | |
118 | vdd->vp_rt_data.vlimitto_vddmax = vdd->pmic_info->vp_vddmax; | |
119 | ||
120 | waittime = ((vdd->pmic_info->step_size / vdd->pmic_info->slew_rate) * | |
121 | sys_clk_speed) / 1000; | |
122 | vdd->vp_rt_data.vstepmin_smpswaittimemin = waittime; | |
123 | vdd->vp_rt_data.vstepmax_smpswaittimemax = waittime; | |
124 | vdd->vp_rt_data.vstepmin_stepmin = vdd->pmic_info->vp_vstepmin; | |
125 | vdd->vp_rt_data.vstepmax_stepmax = vdd->pmic_info->vp_vstepmax; | |
126 | ||
127 | return 0; | |
128 | } | |
129 | ||
077fceca TG |
130 | /* Voltage debugfs support */ |
131 | static int vp_volt_debug_get(void *data, u64 *val) | |
132 | { | |
81a60482 KH |
133 | struct voltagedomain *voltdm = (struct voltagedomain *)data; |
134 | struct omap_vdd_info *vdd = voltdm->vdd; | |
077fceca TG |
135 | u8 vsel; |
136 | ||
137 | if (!vdd) { | |
138 | pr_warning("Wrong paramater passed\n"); | |
139 | return -EINVAL; | |
140 | } | |
141 | ||
a7460daf | 142 | vsel = vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->voltage); |
077fceca TG |
143 | |
144 | if (!vdd->pmic_info->vsel_to_uv) { | |
145 | pr_warning("PMIC function to convert vsel to voltage" | |
146 | "in uV not registerd\n"); | |
147 | return -EINVAL; | |
148 | } | |
149 | ||
150 | *val = vdd->pmic_info->vsel_to_uv(vsel); | |
151 | return 0; | |
152 | } | |
153 | ||
154 | static int nom_volt_debug_get(void *data, u64 *val) | |
155 | { | |
81a60482 | 156 | struct voltagedomain *voltdm = (struct voltagedomain *)data; |
077fceca | 157 | |
81a60482 | 158 | if (!voltdm) { |
077fceca TG |
159 | pr_warning("Wrong paramater passed\n"); |
160 | return -EINVAL; | |
161 | } | |
162 | ||
81a60482 | 163 | *val = omap_voltage_get_nom_volt(voltdm); |
077fceca TG |
164 | |
165 | return 0; | |
166 | } | |
167 | ||
168 | DEFINE_SIMPLE_ATTRIBUTE(vp_volt_debug_fops, vp_volt_debug_get, NULL, "%llu\n"); | |
169 | DEFINE_SIMPLE_ATTRIBUTE(nom_volt_debug_fops, nom_volt_debug_get, NULL, | |
170 | "%llu\n"); | |
81a60482 | 171 | static void vp_latch_vsel(struct voltagedomain *voltdm) |
2f34ce81 TG |
172 | { |
173 | u32 vpconfig; | |
2f34ce81 TG |
174 | unsigned long uvdc; |
175 | char vsel; | |
81a60482 | 176 | struct omap_vdd_info *vdd = voltdm->vdd; |
2f34ce81 | 177 | |
81a60482 | 178 | uvdc = omap_voltage_get_nom_volt(voltdm); |
2f34ce81 TG |
179 | if (!uvdc) { |
180 | pr_warning("%s: unable to find current voltage for vdd_%s\n", | |
81a60482 | 181 | __func__, voltdm->name); |
2f34ce81 TG |
182 | return; |
183 | } | |
184 | ||
185 | if (!vdd->pmic_info || !vdd->pmic_info->uv_to_vsel) { | |
186 | pr_warning("%s: PMIC function to convert voltage in uV to" | |
187 | " vsel not registered\n", __func__); | |
188 | return; | |
189 | } | |
190 | ||
2f34ce81 TG |
191 | vsel = vdd->pmic_info->uv_to_vsel(uvdc); |
192 | ||
a7460daf | 193 | vpconfig = vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
c0718df4 PW |
194 | vpconfig &= ~(vdd->vp_data->vp_common->vpconfig_initvoltage_mask | |
195 | vdd->vp_data->vp_common->vpconfig_initvdd); | |
196 | vpconfig |= vsel << vdd->vp_data->vp_common->vpconfig_initvoltage_shift; | |
2f34ce81 | 197 | |
a7460daf | 198 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
199 | |
200 | /* Trigger initVDD value copy to voltage processor */ | |
c0718df4 | 201 | vdd->write_reg((vpconfig | vdd->vp_data->vp_common->vpconfig_initvdd), |
a7460daf | 202 | vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
203 | |
204 | /* Clear initVDD copy trigger bit */ | |
a7460daf | 205 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
206 | } |
207 | ||
208 | /* Generic voltage init functions */ | |
81a60482 | 209 | static void __init vp_init(struct voltagedomain *voltdm) |
2f34ce81 | 210 | { |
81a60482 | 211 | struct omap_vdd_info *vdd = voltdm->vdd; |
2f34ce81 | 212 | u32 vp_val; |
2f34ce81 TG |
213 | |
214 | if (!vdd->read_reg || !vdd->write_reg) { | |
215 | pr_err("%s: No read/write API for accessing vdd_%s regs\n", | |
81a60482 | 216 | __func__, voltdm->name); |
2f34ce81 TG |
217 | return; |
218 | } | |
219 | ||
c0718df4 PW |
220 | vp_val = vdd->vp_rt_data.vpconfig_erroroffset | |
221 | (vdd->vp_rt_data.vpconfig_errorgain << | |
222 | vdd->vp_data->vp_common->vpconfig_errorgain_shift) | | |
223 | vdd->vp_data->vp_common->vpconfig_timeouten; | |
a7460daf | 224 | vdd->write_reg(vp_val, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
c0718df4 PW |
225 | |
226 | vp_val = ((vdd->vp_rt_data.vstepmin_smpswaittimemin << | |
227 | vdd->vp_data->vp_common->vstepmin_smpswaittimemin_shift) | | |
228 | (vdd->vp_rt_data.vstepmin_stepmin << | |
229 | vdd->vp_data->vp_common->vstepmin_stepmin_shift)); | |
a7460daf | 230 | vdd->write_reg(vp_val, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vstepmin); |
c0718df4 PW |
231 | |
232 | vp_val = ((vdd->vp_rt_data.vstepmax_smpswaittimemax << | |
233 | vdd->vp_data->vp_common->vstepmax_smpswaittimemax_shift) | | |
234 | (vdd->vp_rt_data.vstepmax_stepmax << | |
235 | vdd->vp_data->vp_common->vstepmax_stepmax_shift)); | |
a7460daf | 236 | vdd->write_reg(vp_val, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vstepmax); |
c0718df4 PW |
237 | |
238 | vp_val = ((vdd->vp_rt_data.vlimitto_vddmax << | |
239 | vdd->vp_data->vp_common->vlimitto_vddmax_shift) | | |
240 | (vdd->vp_rt_data.vlimitto_vddmin << | |
241 | vdd->vp_data->vp_common->vlimitto_vddmin_shift) | | |
242 | (vdd->vp_rt_data.vlimitto_timeout << | |
243 | vdd->vp_data->vp_common->vlimitto_timeout_shift)); | |
a7460daf | 244 | vdd->write_reg(vp_val, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vlimitto); |
2f34ce81 TG |
245 | } |
246 | ||
81a60482 | 247 | static void __init vdd_debugfs_init(struct voltagedomain *voltdm) |
2f34ce81 TG |
248 | { |
249 | char *name; | |
81a60482 | 250 | struct omap_vdd_info *vdd = voltdm->vdd; |
2f34ce81 TG |
251 | |
252 | name = kzalloc(VOLTAGE_DIR_SIZE, GFP_KERNEL); | |
253 | if (!name) { | |
254 | pr_warning("%s: Unable to allocate memory for debugfs" | |
255 | " directory name for vdd_%s", | |
81a60482 | 256 | __func__, voltdm->name); |
2f34ce81 TG |
257 | return; |
258 | } | |
259 | strcpy(name, "vdd_"); | |
81a60482 | 260 | strcat(name, voltdm->name); |
2f34ce81 TG |
261 | |
262 | vdd->debug_dir = debugfs_create_dir(name, voltage_dir); | |
62270119 | 263 | kfree(name); |
2f34ce81 TG |
264 | if (IS_ERR(vdd->debug_dir)) { |
265 | pr_warning("%s: Unable to create debugfs directory for" | |
81a60482 | 266 | " vdd_%s\n", __func__, voltdm->name); |
2f34ce81 | 267 | vdd->debug_dir = NULL; |
077fceca | 268 | return; |
2f34ce81 | 269 | } |
077fceca TG |
270 | |
271 | (void) debugfs_create_x16("vp_errorgain", S_IRUGO, vdd->debug_dir, | |
c0718df4 | 272 | &(vdd->vp_rt_data.vpconfig_errorgain)); |
077fceca TG |
273 | (void) debugfs_create_x16("vp_smpswaittimemin", S_IRUGO, |
274 | vdd->debug_dir, | |
c0718df4 | 275 | &(vdd->vp_rt_data.vstepmin_smpswaittimemin)); |
077fceca | 276 | (void) debugfs_create_x8("vp_stepmin", S_IRUGO, vdd->debug_dir, |
c0718df4 | 277 | &(vdd->vp_rt_data.vstepmin_stepmin)); |
077fceca TG |
278 | (void) debugfs_create_x16("vp_smpswaittimemax", S_IRUGO, |
279 | vdd->debug_dir, | |
c0718df4 | 280 | &(vdd->vp_rt_data.vstepmax_smpswaittimemax)); |
077fceca | 281 | (void) debugfs_create_x8("vp_stepmax", S_IRUGO, vdd->debug_dir, |
c0718df4 | 282 | &(vdd->vp_rt_data.vstepmax_stepmax)); |
077fceca | 283 | (void) debugfs_create_x8("vp_vddmax", S_IRUGO, vdd->debug_dir, |
c0718df4 | 284 | &(vdd->vp_rt_data.vlimitto_vddmax)); |
077fceca | 285 | (void) debugfs_create_x8("vp_vddmin", S_IRUGO, vdd->debug_dir, |
c0718df4 | 286 | &(vdd->vp_rt_data.vlimitto_vddmin)); |
077fceca | 287 | (void) debugfs_create_x16("vp_timeout", S_IRUGO, vdd->debug_dir, |
c0718df4 | 288 | &(vdd->vp_rt_data.vlimitto_timeout)); |
077fceca | 289 | (void) debugfs_create_file("curr_vp_volt", S_IRUGO, vdd->debug_dir, |
81a60482 | 290 | (void *) voltdm, &vp_volt_debug_fops); |
077fceca | 291 | (void) debugfs_create_file("curr_nominal_volt", S_IRUGO, |
81a60482 | 292 | vdd->debug_dir, (void *) voltdm, |
077fceca | 293 | &nom_volt_debug_fops); |
2f34ce81 TG |
294 | } |
295 | ||
2f34ce81 | 296 | /* VP force update method of voltage scaling */ |
81a60482 | 297 | static int vp_forceupdate_scale_voltage(struct voltagedomain *voltdm, |
2f34ce81 TG |
298 | unsigned long target_volt) |
299 | { | |
81a60482 | 300 | struct omap_vdd_info *vdd = voltdm->vdd; |
2f34ce81 | 301 | u32 vpconfig; |
c39263c3 | 302 | u8 target_vsel, current_vsel; |
2f34ce81 TG |
303 | int ret, timeout = 0; |
304 | ||
ccd5ca77 | 305 | ret = omap_vc_pre_scale(voltdm, target_volt, &target_vsel, ¤t_vsel); |
2f34ce81 TG |
306 | if (ret) |
307 | return ret; | |
308 | ||
2f34ce81 TG |
309 | /* |
310 | * Clear all pending TransactionDone interrupt/status. Typical latency | |
311 | * is <3us | |
312 | */ | |
313 | while (timeout++ < VP_TRANXDONE_TIMEOUT) { | |
c0718df4 | 314 | vdd->write_reg(vdd->vp_data->prm_irqst_data->tranxdone_status, |
c39263c3 KH |
315 | vdd->prm_irqst_mod, vdd->prm_irqst_reg); |
316 | if (!(vdd->read_reg(vdd->prm_irqst_mod, vdd->prm_irqst_reg) & | |
c0718df4 PW |
317 | vdd->vp_data->prm_irqst_data->tranxdone_status)) |
318 | break; | |
2f34ce81 TG |
319 | udelay(1); |
320 | } | |
321 | if (timeout >= VP_TRANXDONE_TIMEOUT) { | |
322 | pr_warning("%s: vdd_%s TRANXDONE timeout exceeded." | |
81a60482 | 323 | "Voltage change aborted", __func__, voltdm->name); |
2f34ce81 TG |
324 | return -ETIMEDOUT; |
325 | } | |
326 | ||
327 | /* Configure for VP-Force Update */ | |
a7460daf | 328 | vpconfig = vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
c0718df4 PW |
329 | vpconfig &= ~(vdd->vp_data->vp_common->vpconfig_initvdd | |
330 | vdd->vp_data->vp_common->vpconfig_forceupdate | | |
331 | vdd->vp_data->vp_common->vpconfig_initvoltage_mask); | |
2f34ce81 | 332 | vpconfig |= ((target_vsel << |
c0718df4 | 333 | vdd->vp_data->vp_common->vpconfig_initvoltage_shift)); |
a7460daf | 334 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
335 | |
336 | /* Trigger initVDD value copy to voltage processor */ | |
c0718df4 | 337 | vpconfig |= vdd->vp_data->vp_common->vpconfig_initvdd; |
a7460daf | 338 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
339 | |
340 | /* Force update of voltage */ | |
c0718df4 | 341 | vpconfig |= vdd->vp_data->vp_common->vpconfig_forceupdate; |
a7460daf | 342 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
343 | |
344 | /* | |
345 | * Wait for TransactionDone. Typical latency is <200us. | |
346 | * Depends on SMPSWAITTIMEMIN/MAX and voltage change | |
347 | */ | |
348 | timeout = 0; | |
c39263c3 KH |
349 | omap_test_timeout((vdd->read_reg(vdd->prm_irqst_mod, |
350 | vdd->prm_irqst_reg) & | |
c0718df4 PW |
351 | vdd->vp_data->prm_irqst_data->tranxdone_status), |
352 | VP_TRANXDONE_TIMEOUT, timeout); | |
2f34ce81 TG |
353 | if (timeout >= VP_TRANXDONE_TIMEOUT) |
354 | pr_err("%s: vdd_%s TRANXDONE timeout exceeded." | |
355 | "TRANXDONE never got set after the voltage update\n", | |
81a60482 | 356 | __func__, voltdm->name); |
2f34ce81 | 357 | |
ccd5ca77 | 358 | omap_vc_post_scale(voltdm, target_volt, target_vsel, current_vsel); |
2f34ce81 TG |
359 | |
360 | /* | |
361 | * Disable TransactionDone interrupt , clear all status, clear | |
362 | * control registers | |
363 | */ | |
364 | timeout = 0; | |
365 | while (timeout++ < VP_TRANXDONE_TIMEOUT) { | |
c0718df4 | 366 | vdd->write_reg(vdd->vp_data->prm_irqst_data->tranxdone_status, |
c39263c3 KH |
367 | vdd->prm_irqst_mod, vdd->prm_irqst_reg); |
368 | if (!(vdd->read_reg(vdd->prm_irqst_mod, vdd->prm_irqst_reg) & | |
c0718df4 PW |
369 | vdd->vp_data->prm_irqst_data->tranxdone_status)) |
370 | break; | |
2f34ce81 TG |
371 | udelay(1); |
372 | } | |
373 | ||
374 | if (timeout >= VP_TRANXDONE_TIMEOUT) | |
375 | pr_warning("%s: vdd_%s TRANXDONE timeout exceeded while trying" | |
376 | "to clear the TRANXDONE status\n", | |
81a60482 | 377 | __func__, voltdm->name); |
2f34ce81 | 378 | |
a7460daf | 379 | vpconfig = vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 | 380 | /* Clear initVDD copy trigger bit */ |
c0718df4 | 381 | vpconfig &= ~vdd->vp_data->vp_common->vpconfig_initvdd; |
a7460daf | 382 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 | 383 | /* Clear force bit */ |
c0718df4 | 384 | vpconfig &= ~vdd->vp_data->vp_common->vpconfig_forceupdate; |
a7460daf | 385 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
386 | |
387 | return 0; | |
388 | } | |
389 | ||
81a60482 | 390 | static int __init omap_vdd_data_configure(struct voltagedomain *voltdm) |
bd38107b | 391 | { |
81a60482 | 392 | struct omap_vdd_info *vdd = voltdm->vdd; |
c0718df4 | 393 | int ret = -EINVAL; |
bd38107b TG |
394 | |
395 | if (!vdd->pmic_info) { | |
396 | pr_err("%s: PMIC info requried to configure vdd_%s not" | |
397 | "populated.Hence cannot initialize vdd_%s\n", | |
81a60482 | 398 | __func__, voltdm->name, voltdm->name); |
c0718df4 | 399 | goto ovdc_out; |
bd38107b TG |
400 | } |
401 | ||
81a60482 | 402 | if (IS_ERR_VALUE(_config_common_vdd_data(voltdm))) |
c0718df4 | 403 | goto ovdc_out; |
bd38107b | 404 | |
c0718df4 PW |
405 | if (cpu_is_omap34xx()) { |
406 | vdd->read_reg = omap3_voltage_read_reg; | |
407 | vdd->write_reg = omap3_voltage_write_reg; | |
408 | ret = 0; | |
409 | } else if (cpu_is_omap44xx()) { | |
410 | vdd->read_reg = omap4_voltage_read_reg; | |
411 | vdd->write_reg = omap4_voltage_write_reg; | |
412 | ret = 0; | |
bd38107b | 413 | } |
bd38107b | 414 | |
c0718df4 PW |
415 | ovdc_out: |
416 | return ret; | |
bd38107b TG |
417 | } |
418 | ||
2f34ce81 TG |
419 | /* Public functions */ |
420 | /** | |
421 | * omap_voltage_get_nom_volt() - Gets the current non-auto-compensated voltage | |
422 | * @voltdm: pointer to the VDD for which current voltage info is needed | |
423 | * | |
424 | * API to get the current non-auto-compensated voltage for a VDD. | |
425 | * Returns 0 in case of error else returns the current voltage for the VDD. | |
426 | */ | |
427 | unsigned long omap_voltage_get_nom_volt(struct voltagedomain *voltdm) | |
428 | { | |
429 | struct omap_vdd_info *vdd; | |
430 | ||
431 | if (!voltdm || IS_ERR(voltdm)) { | |
432 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
433 | return 0; | |
434 | } | |
435 | ||
81a60482 | 436 | vdd = voltdm->vdd; |
2f34ce81 TG |
437 | |
438 | return vdd->curr_volt; | |
439 | } | |
440 | ||
441 | /** | |
442 | * omap_vp_get_curr_volt() - API to get the current vp voltage. | |
443 | * @voltdm: pointer to the VDD. | |
444 | * | |
445 | * This API returns the current voltage for the specified voltage processor | |
446 | */ | |
447 | unsigned long omap_vp_get_curr_volt(struct voltagedomain *voltdm) | |
448 | { | |
449 | struct omap_vdd_info *vdd; | |
450 | u8 curr_vsel; | |
451 | ||
452 | if (!voltdm || IS_ERR(voltdm)) { | |
453 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
454 | return 0; | |
455 | } | |
456 | ||
81a60482 | 457 | vdd = voltdm->vdd; |
2f34ce81 TG |
458 | if (!vdd->read_reg) { |
459 | pr_err("%s: No read API for reading vdd_%s regs\n", | |
460 | __func__, voltdm->name); | |
461 | return 0; | |
462 | } | |
463 | ||
a7460daf | 464 | curr_vsel = vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->voltage); |
2f34ce81 TG |
465 | |
466 | if (!vdd->pmic_info || !vdd->pmic_info->vsel_to_uv) { | |
467 | pr_warning("%s: PMIC function to convert vsel to voltage" | |
468 | "in uV not registerd\n", __func__); | |
469 | return 0; | |
470 | } | |
471 | ||
472 | return vdd->pmic_info->vsel_to_uv(curr_vsel); | |
473 | } | |
474 | ||
475 | /** | |
476 | * omap_vp_enable() - API to enable a particular VP | |
477 | * @voltdm: pointer to the VDD whose VP is to be enabled. | |
478 | * | |
479 | * This API enables a particular voltage processor. Needed by the smartreflex | |
480 | * class drivers. | |
481 | */ | |
482 | void omap_vp_enable(struct voltagedomain *voltdm) | |
483 | { | |
484 | struct omap_vdd_info *vdd; | |
485 | u32 vpconfig; | |
2f34ce81 TG |
486 | |
487 | if (!voltdm || IS_ERR(voltdm)) { | |
488 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
489 | return; | |
490 | } | |
491 | ||
81a60482 | 492 | vdd = voltdm->vdd; |
2f34ce81 TG |
493 | if (!vdd->read_reg || !vdd->write_reg) { |
494 | pr_err("%s: No read/write API for accessing vdd_%s regs\n", | |
495 | __func__, voltdm->name); | |
496 | return; | |
497 | } | |
498 | ||
2f34ce81 TG |
499 | /* If VP is already enabled, do nothing. Return */ |
500 | if (vdd->vp_enabled) | |
501 | return; | |
502 | ||
81a60482 | 503 | vp_latch_vsel(voltdm); |
2f34ce81 TG |
504 | |
505 | /* Enable VP */ | |
a7460daf | 506 | vpconfig = vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
c0718df4 | 507 | vpconfig |= vdd->vp_data->vp_common->vpconfig_vpenable; |
a7460daf | 508 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
509 | vdd->vp_enabled = true; |
510 | } | |
511 | ||
512 | /** | |
513 | * omap_vp_disable() - API to disable a particular VP | |
514 | * @voltdm: pointer to the VDD whose VP is to be disabled. | |
515 | * | |
516 | * This API disables a particular voltage processor. Needed by the smartreflex | |
517 | * class drivers. | |
518 | */ | |
519 | void omap_vp_disable(struct voltagedomain *voltdm) | |
520 | { | |
521 | struct omap_vdd_info *vdd; | |
522 | u32 vpconfig; | |
2f34ce81 TG |
523 | int timeout; |
524 | ||
525 | if (!voltdm || IS_ERR(voltdm)) { | |
526 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
527 | return; | |
528 | } | |
529 | ||
81a60482 | 530 | vdd = voltdm->vdd; |
2f34ce81 TG |
531 | if (!vdd->read_reg || !vdd->write_reg) { |
532 | pr_err("%s: No read/write API for accessing vdd_%s regs\n", | |
533 | __func__, voltdm->name); | |
534 | return; | |
535 | } | |
536 | ||
2f34ce81 TG |
537 | /* If VP is already disabled, do nothing. Return */ |
538 | if (!vdd->vp_enabled) { | |
539 | pr_warning("%s: Trying to disable VP for vdd_%s when" | |
540 | "it is already disabled\n", __func__, voltdm->name); | |
541 | return; | |
542 | } | |
543 | ||
544 | /* Disable VP */ | |
a7460daf | 545 | vpconfig = vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
c0718df4 | 546 | vpconfig &= ~vdd->vp_data->vp_common->vpconfig_vpenable; |
a7460daf | 547 | vdd->write_reg(vpconfig, vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vpconfig); |
2f34ce81 TG |
548 | |
549 | /* | |
550 | * Wait for VP idle Typical latency is <2us. Maximum latency is ~100us | |
551 | */ | |
a7460daf | 552 | omap_test_timeout((vdd->read_reg(vdd->vp_data->vp_common->prm_mod, vdd->vp_data->vstatus)), |
2f34ce81 TG |
553 | VP_IDLE_TIMEOUT, timeout); |
554 | ||
555 | if (timeout >= VP_IDLE_TIMEOUT) | |
556 | pr_warning("%s: vdd_%s idle timedout\n", | |
557 | __func__, voltdm->name); | |
558 | ||
559 | vdd->vp_enabled = false; | |
560 | ||
561 | return; | |
562 | } | |
563 | ||
564 | /** | |
565 | * omap_voltage_scale_vdd() - API to scale voltage of a particular | |
566 | * voltage domain. | |
567 | * @voltdm: pointer to the VDD which is to be scaled. | |
568 | * @target_volt: The target voltage of the voltage domain | |
569 | * | |
570 | * This API should be called by the kernel to do the voltage scaling | |
571 | * for a particular voltage domain during dvfs or any other situation. | |
572 | */ | |
573 | int omap_voltage_scale_vdd(struct voltagedomain *voltdm, | |
574 | unsigned long target_volt) | |
575 | { | |
576 | struct omap_vdd_info *vdd; | |
577 | ||
578 | if (!voltdm || IS_ERR(voltdm)) { | |
579 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
580 | return -EINVAL; | |
581 | } | |
582 | ||
81a60482 | 583 | vdd = voltdm->vdd; |
2f34ce81 TG |
584 | |
585 | if (!vdd->volt_scale) { | |
586 | pr_err("%s: No voltage scale API registered for vdd_%s\n", | |
587 | __func__, voltdm->name); | |
588 | return -ENODATA; | |
589 | } | |
590 | ||
81a60482 | 591 | return vdd->volt_scale(voltdm, target_volt); |
2f34ce81 TG |
592 | } |
593 | ||
594 | /** | |
595 | * omap_voltage_reset() - Resets the voltage of a particular voltage domain | |
596 | * to that of the current OPP. | |
597 | * @voltdm: pointer to the VDD whose voltage is to be reset. | |
598 | * | |
599 | * This API finds out the correct voltage the voltage domain is supposed | |
25985edc | 600 | * to be at and resets the voltage to that level. Should be used especially |
2f34ce81 TG |
601 | * while disabling any voltage compensation modules. |
602 | */ | |
603 | void omap_voltage_reset(struct voltagedomain *voltdm) | |
604 | { | |
605 | unsigned long target_uvdc; | |
606 | ||
607 | if (!voltdm || IS_ERR(voltdm)) { | |
608 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
609 | return; | |
610 | } | |
611 | ||
612 | target_uvdc = omap_voltage_get_nom_volt(voltdm); | |
613 | if (!target_uvdc) { | |
614 | pr_err("%s: unable to find current voltage for vdd_%s\n", | |
615 | __func__, voltdm->name); | |
616 | return; | |
617 | } | |
618 | ||
619 | omap_voltage_scale_vdd(voltdm, target_uvdc); | |
620 | } | |
621 | ||
622 | /** | |
623 | * omap_voltage_get_volttable() - API to get the voltage table associated with a | |
624 | * particular voltage domain. | |
625 | * @voltdm: pointer to the VDD for which the voltage table is required | |
626 | * @volt_data: the voltage table for the particular vdd which is to be | |
627 | * populated by this API | |
628 | * | |
629 | * This API populates the voltage table associated with a VDD into the | |
630 | * passed parameter pointer. Returns the count of distinct voltages | |
631 | * supported by this vdd. | |
632 | * | |
633 | */ | |
634 | void omap_voltage_get_volttable(struct voltagedomain *voltdm, | |
635 | struct omap_volt_data **volt_data) | |
636 | { | |
637 | struct omap_vdd_info *vdd; | |
638 | ||
639 | if (!voltdm || IS_ERR(voltdm)) { | |
640 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
641 | return; | |
642 | } | |
643 | ||
81a60482 | 644 | vdd = voltdm->vdd; |
2f34ce81 TG |
645 | |
646 | *volt_data = vdd->volt_data; | |
647 | } | |
648 | ||
649 | /** | |
650 | * omap_voltage_get_voltdata() - API to get the voltage table entry for a | |
651 | * particular voltage | |
652 | * @voltdm: pointer to the VDD whose voltage table has to be searched | |
653 | * @volt: the voltage to be searched in the voltage table | |
654 | * | |
655 | * This API searches through the voltage table for the required voltage | |
656 | * domain and tries to find a matching entry for the passed voltage volt. | |
657 | * If a matching entry is found volt_data is populated with that entry. | |
658 | * This API searches only through the non-compensated voltages int the | |
659 | * voltage table. | |
660 | * Returns pointer to the voltage table entry corresponding to volt on | |
25985edc | 661 | * success. Returns -ENODATA if no voltage table exisits for the passed voltage |
2f34ce81 TG |
662 | * domain or if there is no matching entry. |
663 | */ | |
664 | struct omap_volt_data *omap_voltage_get_voltdata(struct voltagedomain *voltdm, | |
665 | unsigned long volt) | |
666 | { | |
667 | struct omap_vdd_info *vdd; | |
668 | int i; | |
669 | ||
670 | if (!voltdm || IS_ERR(voltdm)) { | |
671 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
672 | return ERR_PTR(-EINVAL); | |
673 | } | |
674 | ||
81a60482 | 675 | vdd = voltdm->vdd; |
2f34ce81 TG |
676 | |
677 | if (!vdd->volt_data) { | |
678 | pr_warning("%s: voltage table does not exist for vdd_%s\n", | |
679 | __func__, voltdm->name); | |
680 | return ERR_PTR(-ENODATA); | |
681 | } | |
682 | ||
683 | for (i = 0; vdd->volt_data[i].volt_nominal != 0; i++) { | |
684 | if (vdd->volt_data[i].volt_nominal == volt) | |
685 | return &vdd->volt_data[i]; | |
686 | } | |
687 | ||
688 | pr_notice("%s: Unable to match the current voltage with the voltage" | |
689 | "table for vdd_%s\n", __func__, voltdm->name); | |
690 | ||
691 | return ERR_PTR(-ENODATA); | |
692 | } | |
693 | ||
694 | /** | |
695 | * omap_voltage_register_pmic() - API to register PMIC specific data | |
696 | * @voltdm: pointer to the VDD for which the PMIC specific data is | |
697 | * to be registered | |
698 | * @pmic_info: the structure containing pmic info | |
699 | * | |
700 | * This API is to be called by the SOC/PMIC file to specify the | |
701 | * pmic specific info as present in omap_volt_pmic_info structure. | |
702 | */ | |
703 | int omap_voltage_register_pmic(struct voltagedomain *voltdm, | |
704 | struct omap_volt_pmic_info *pmic_info) | |
705 | { | |
706 | struct omap_vdd_info *vdd; | |
707 | ||
708 | if (!voltdm || IS_ERR(voltdm)) { | |
709 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
710 | return -EINVAL; | |
711 | } | |
712 | ||
81a60482 | 713 | vdd = voltdm->vdd; |
2f34ce81 TG |
714 | |
715 | vdd->pmic_info = pmic_info; | |
716 | ||
717 | return 0; | |
718 | } | |
719 | ||
720 | /** | |
721 | * omap_voltage_get_dbgdir() - API to get pointer to the debugfs directory | |
722 | * corresponding to a voltage domain. | |
723 | * | |
724 | * @voltdm: pointer to the VDD whose debug directory is required. | |
725 | * | |
726 | * This API returns pointer to the debugfs directory corresponding | |
727 | * to the voltage domain. Should be used by drivers requiring to | |
728 | * add any debug entry for a particular voltage domain. Returns NULL | |
729 | * in case of error. | |
730 | */ | |
731 | struct dentry *omap_voltage_get_dbgdir(struct voltagedomain *voltdm) | |
732 | { | |
733 | struct omap_vdd_info *vdd; | |
734 | ||
735 | if (!voltdm || IS_ERR(voltdm)) { | |
736 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
737 | return NULL; | |
738 | } | |
739 | ||
81a60482 | 740 | vdd = voltdm->vdd; |
2f34ce81 TG |
741 | |
742 | return vdd->debug_dir; | |
743 | } | |
744 | ||
745 | /** | |
746 | * omap_change_voltscale_method() - API to change the voltage scaling method. | |
747 | * @voltdm: pointer to the VDD whose voltage scaling method | |
748 | * has to be changed. | |
749 | * @voltscale_method: the method to be used for voltage scaling. | |
750 | * | |
751 | * This API can be used by the board files to change the method of voltage | |
752 | * scaling between vpforceupdate and vcbypass. The parameter values are | |
753 | * defined in voltage.h | |
754 | */ | |
755 | void omap_change_voltscale_method(struct voltagedomain *voltdm, | |
756 | int voltscale_method) | |
757 | { | |
758 | struct omap_vdd_info *vdd; | |
759 | ||
760 | if (!voltdm || IS_ERR(voltdm)) { | |
761 | pr_warning("%s: VDD specified does not exist!\n", __func__); | |
762 | return; | |
763 | } | |
764 | ||
81a60482 | 765 | vdd = voltdm->vdd; |
2f34ce81 TG |
766 | |
767 | switch (voltscale_method) { | |
768 | case VOLTSCALE_VPFORCEUPDATE: | |
769 | vdd->volt_scale = vp_forceupdate_scale_voltage; | |
770 | return; | |
771 | case VOLTSCALE_VCBYPASS: | |
d84adcf4 | 772 | vdd->volt_scale = omap_vc_bypass_scale; |
2f34ce81 TG |
773 | return; |
774 | default: | |
775 | pr_warning("%s: Trying to change the method of voltage scaling" | |
776 | "to an unsupported one!\n", __func__); | |
777 | } | |
778 | } | |
779 | ||
2f34ce81 TG |
780 | /** |
781 | * omap_voltage_late_init() - Init the various voltage parameters | |
782 | * | |
783 | * This API is to be called in the later stages of the | |
784 | * system boot to init the voltage controller and | |
785 | * voltage processors. | |
786 | */ | |
787 | int __init omap_voltage_late_init(void) | |
788 | { | |
81a60482 | 789 | struct voltagedomain *voltdm; |
2f34ce81 | 790 | |
81a60482 | 791 | if (list_empty(&voltdm_list)) { |
2f34ce81 TG |
792 | pr_err("%s: Voltage driver support not added\n", |
793 | __func__); | |
794 | return -EINVAL; | |
795 | } | |
796 | ||
797 | voltage_dir = debugfs_create_dir("voltage", NULL); | |
798 | if (IS_ERR(voltage_dir)) | |
799 | pr_err("%s: Unable to create voltage debugfs main dir\n", | |
800 | __func__); | |
81a60482 | 801 | list_for_each_entry(voltdm, &voltdm_list, node) { |
37efca7e KH |
802 | if (!voltdm->scalable) |
803 | continue; | |
804 | ||
4d47506a KH |
805 | if (voltdm->vc) { |
806 | voltdm->vdd->volt_scale = omap_vc_bypass_scale; | |
d84adcf4 | 807 | omap_vc_init_channel(voltdm); |
4d47506a | 808 | } |
d84adcf4 | 809 | |
81a60482 KH |
810 | if (voltdm->vdd) { |
811 | if (omap_vdd_data_configure(voltdm)) | |
812 | continue; | |
81a60482 KH |
813 | vp_init(voltdm); |
814 | vdd_debugfs_init(voltdm); | |
815 | } | |
2f34ce81 TG |
816 | } |
817 | ||
818 | return 0; | |
819 | } | |
820 | ||
81a60482 | 821 | static struct voltagedomain *_voltdm_lookup(const char *name) |
2f34ce81 | 822 | { |
81a60482 KH |
823 | struct voltagedomain *voltdm, *temp_voltdm; |
824 | ||
825 | voltdm = NULL; | |
826 | ||
827 | list_for_each_entry(temp_voltdm, &voltdm_list, node) { | |
828 | if (!strcmp(name, temp_voltdm->name)) { | |
829 | voltdm = temp_voltdm; | |
830 | break; | |
831 | } | |
832 | } | |
833 | ||
834 | return voltdm; | |
835 | } | |
836 | ||
e69c22b1 KH |
837 | /** |
838 | * voltdm_add_pwrdm - add a powerdomain to a voltagedomain | |
839 | * @voltdm: struct voltagedomain * to add the powerdomain to | |
840 | * @pwrdm: struct powerdomain * to associate with a voltagedomain | |
841 | * | |
842 | * Associate the powerdomain @pwrdm with a voltagedomain @voltdm. This | |
843 | * enables the use of voltdm_for_each_pwrdm(). Returns -EINVAL if | |
844 | * presented with invalid pointers; -ENOMEM if memory could not be allocated; | |
845 | * or 0 upon success. | |
846 | */ | |
847 | int voltdm_add_pwrdm(struct voltagedomain *voltdm, struct powerdomain *pwrdm) | |
848 | { | |
849 | if (!voltdm || !pwrdm) | |
850 | return -EINVAL; | |
851 | ||
852 | pr_debug("voltagedomain: associating powerdomain %s with voltagedomain " | |
853 | "%s\n", pwrdm->name, voltdm->name); | |
854 | ||
855 | list_add(&pwrdm->voltdm_node, &voltdm->pwrdm_list); | |
856 | ||
857 | return 0; | |
858 | } | |
859 | ||
860 | /** | |
861 | * voltdm_for_each_pwrdm - call function for each pwrdm in a voltdm | |
862 | * @voltdm: struct voltagedomain * to iterate over | |
863 | * @fn: callback function * | |
864 | * | |
865 | * Call the supplied function @fn for each powerdomain in the | |
866 | * voltagedomain @voltdm. Returns -EINVAL if presented with invalid | |
867 | * pointers; or passes along the last return value of the callback | |
868 | * function, which should be 0 for success or anything else to | |
869 | * indicate failure. | |
870 | */ | |
871 | int voltdm_for_each_pwrdm(struct voltagedomain *voltdm, | |
872 | int (*fn)(struct voltagedomain *voltdm, | |
873 | struct powerdomain *pwrdm)) | |
874 | { | |
875 | struct powerdomain *pwrdm; | |
876 | int ret = 0; | |
877 | ||
878 | if (!fn) | |
879 | return -EINVAL; | |
880 | ||
881 | list_for_each_entry(pwrdm, &voltdm->pwrdm_list, voltdm_node) | |
882 | ret = (*fn)(voltdm, pwrdm); | |
883 | ||
884 | return ret; | |
885 | } | |
886 | ||
887 | /** | |
888 | * voltdm_for_each - call function on each registered voltagedomain | |
889 | * @fn: callback function * | |
890 | * | |
891 | * Call the supplied function @fn for each registered voltagedomain. | |
892 | * The callback function @fn can return anything but 0 to bail out | |
893 | * early from the iterator. Returns the last return value of the | |
894 | * callback function, which should be 0 for success or anything else | |
895 | * to indicate failure; or -EINVAL if the function pointer is null. | |
896 | */ | |
897 | int voltdm_for_each(int (*fn)(struct voltagedomain *voltdm, void *user), | |
898 | void *user) | |
899 | { | |
900 | struct voltagedomain *temp_voltdm; | |
901 | int ret = 0; | |
902 | ||
903 | if (!fn) | |
904 | return -EINVAL; | |
905 | ||
906 | list_for_each_entry(temp_voltdm, &voltdm_list, node) { | |
907 | ret = (*fn)(temp_voltdm, user); | |
908 | if (ret) | |
909 | break; | |
910 | } | |
911 | ||
912 | return ret; | |
913 | } | |
914 | ||
81a60482 KH |
915 | static int _voltdm_register(struct voltagedomain *voltdm) |
916 | { | |
917 | if (!voltdm || !voltdm->name) | |
918 | return -EINVAL; | |
919 | ||
e69c22b1 | 920 | INIT_LIST_HEAD(&voltdm->pwrdm_list); |
81a60482 KH |
921 | list_add(&voltdm->node, &voltdm_list); |
922 | ||
923 | pr_debug("voltagedomain: registered %s\n", voltdm->name); | |
924 | ||
2f34ce81 TG |
925 | return 0; |
926 | } | |
81a60482 KH |
927 | |
928 | /** | |
929 | * voltdm_lookup - look up a voltagedomain by name, return a pointer | |
930 | * @name: name of voltagedomain | |
931 | * | |
932 | * Find a registered voltagedomain by its name @name. Returns a pointer | |
933 | * to the struct voltagedomain if found, or NULL otherwise. | |
934 | */ | |
935 | struct voltagedomain *voltdm_lookup(const char *name) | |
936 | { | |
937 | struct voltagedomain *voltdm ; | |
938 | ||
939 | if (!name) | |
940 | return NULL; | |
941 | ||
942 | voltdm = _voltdm_lookup(name); | |
943 | ||
944 | return voltdm; | |
945 | } | |
946 | ||
947 | /** | |
948 | * voltdm_init - set up the voltagedomain layer | |
949 | * @voltdm_list: array of struct voltagedomain pointers to register | |
950 | * | |
951 | * Loop through the array of voltagedomains @voltdm_list, registering all | |
952 | * that are available on the current CPU. If voltdm_list is supplied | |
953 | * and not null, all of the referenced voltagedomains will be | |
954 | * registered. No return value. | |
955 | */ | |
956 | void voltdm_init(struct voltagedomain **voltdms) | |
957 | { | |
958 | struct voltagedomain **v; | |
959 | ||
960 | if (voltdms) { | |
961 | for (v = voltdms; *v; v++) | |
962 | _voltdm_register(*v); | |
963 | } | |
964 | } |