Skip to content

Commit 938693a

Browse files
committed
Refactor power supply
- Add new module power_supply.py - Add PowerSupply class - Voltage and current now set via properties - Document limitations of current source - Add tests
1 parent cc9ad27 commit 938693a

File tree

9 files changed

+240
-214
lines changed

9 files changed

+240
-214
lines changed

PSL/Peripherals.py

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -280,128 +280,6 @@ def xfer(self, chan, data):
280280
return reply
281281

282282

283-
class DACCHAN:
284-
def __init__(self, name, span, channum, **kwargs):
285-
self.name = name
286-
self.channum = channum
287-
self.VREF = kwargs.get('VREF', 0)
288-
self.SwitchedOff = kwargs.get('STATE', 0)
289-
self.range = span
290-
slope = (span[1] - span[0])
291-
intercept = span[0]
292-
self.VToCode = np.poly1d([4095. / slope, -4095. * intercept / slope])
293-
self.CodeToV = np.poly1d([slope / 4095., intercept])
294-
self.calibration_enabled = False
295-
self.calibration_table = []
296-
self.slope = 1
297-
self.offset = 0
298-
299-
def load_calibration_table(self, table):
300-
self.calibration_enabled = 'table'
301-
self.calibration_table = table
302-
303-
def load_calibration_twopoint(self, slope, offset):
304-
self.calibration_enabled = 'twopoint'
305-
self.slope = slope
306-
self.offset = offset
307-
308-
# print('########################',slope,offset)
309-
310-
def apply_calibration(self, v):
311-
if self.calibration_enabled == 'table': # Each point is individually calibrated
312-
return int(np.clip(v + self.calibration_table[v], 0, 4095))
313-
elif self.calibration_enabled == 'twopoint': # Overall slope and offset correction is applied
314-
# print (self.slope,self.offset,v)
315-
return int(np.clip(v * self.slope + self.offset, 0, 4095))
316-
else:
317-
return v
318-
319-
320-
class MCP4728:
321-
defaultVDD = 3300
322-
RESET = 6
323-
WAKEUP = 9
324-
UPDATE = 8
325-
WRITEALL = 64
326-
WRITEONE = 88
327-
SEQWRITE = 80
328-
VREFWRITE = 128
329-
GAINWRITE = 192
330-
POWERDOWNWRITE = 160
331-
GENERALCALL = 0
332-
333-
# def __init__(self,I2C,vref=3.3,devid=0):
334-
def __init__(self, H, vref=3.3, devid=0):
335-
self.devid = devid
336-
self.addr = 0x60 | self.devid # 0x60 is the base address
337-
self.H = H
338-
self.I2C = I2C(self.H)
339-
self.SWITCHEDOFF = [0, 0, 0, 0]
340-
self.VREFS = [0, 0, 0, 0] # 0=Vdd,1=Internal reference
341-
self.CHANS = {'PCS': DACCHAN('PCS', [0, 3.3e-3], 0), 'PV3': DACCHAN('PV3', [0, 3.3], 1),
342-
'PV2': DACCHAN('PV2', [-3.3, 3.3], 2), 'PV1': DACCHAN('PV1', [-5., 5.], 3)}
343-
self.CHANNEL_MAP = {0: 'PCS', 1: 'PV3', 2: 'PV2', 3: 'PV1'}
344-
self.values = {'PV1': 0, 'PV2': 0, 'PV3': 0, 'PCS': 0}
345-
346-
def __ignoreCalibration__(self, name):
347-
self.CHANS[name].calibration_enabled = False
348-
349-
def setVoltage(self, name, v):
350-
chan = self.CHANS[name]
351-
v = int(round(chan.VToCode(v)))
352-
return self.__setRawVoltage__(name, v)
353-
354-
def getVoltage(self, name):
355-
return self.values[name]
356-
357-
def setCurrent(self, v):
358-
chan = self.CHANS['PCS']
359-
v = int(round(chan.VToCode(v)))
360-
return self.__setRawVoltage__('PCS', v)
361-
362-
def __setRawVoltage__(self, name, v):
363-
v = int(np.clip(v, 0, 4095))
364-
CHAN = self.CHANS[name]
365-
'''
366-
self.H.__sendByte__(CP.DAC) #DAC write coming through.(MCP4728)
367-
self.H.__sendByte__(CP.SET_DAC)
368-
self.H.__sendByte__(self.addr<<1) #I2C address
369-
self.H.__sendByte__(CHAN.channum) #DAC channel
370-
if self.calibration_enabled[name]:
371-
val = v+self.calibration_tables[name][v]
372-
#print (val,v,self.calibration_tables[name][v])
373-
self.H.__sendInt__((CHAN.VREF << 15) | (CHAN.SwitchedOff << 13) | (0 << 12) | (val) )
374-
else:
375-
self.H.__sendInt__((CHAN.VREF << 15) | (CHAN.SwitchedOff << 13) | (0 << 12) | v )
376-
377-
self.H.__get_ack__()
378-
'''
379-
val = self.CHANS[name].apply_calibration(v)
380-
self.I2C.writeBulk(self.addr, [64 | (CHAN.channum << 1), (val >> 8) & 0x0F, val & 0xFF])
381-
self.values[name] = CHAN.CodeToV(v)
382-
return self.values[name]
383-
384-
def __writeall__(self, v1, v2, v3, v4):
385-
self.I2C.start(self.addr, 0)
386-
self.I2C.send((v1 >> 8) & 0xF)
387-
self.I2C.send(v1 & 0xFF)
388-
self.I2C.send((v2 >> 8) & 0xF)
389-
self.I2C.send(v2 & 0xFF)
390-
self.I2C.send((v3 >> 8) & 0xF)
391-
self.I2C.send(v3 & 0xFF)
392-
self.I2C.send((v4 >> 8) & 0xF)
393-
self.I2C.send(v4 & 0xFF)
394-
self.I2C.stop()
395-
396-
def stat(self):
397-
self.I2C.start(self.addr, 0)
398-
self.I2C.send(0x0) # read raw values starting from address
399-
self.I2C.restart(self.addr, 1)
400-
vals = self.I2C.read(24)
401-
self.I2C.stop()
402-
print(vals)
403-
404-
405283
class NRF24L01():
406284
# Commands
407285
R_REG = 0x00

PSL/power_supply.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Control voltage and current with the PSLab's PV1, PV2, PV3, and PCS pins.
2+
3+
Examples
4+
--------
5+
>>> from PSL.power_supply import PowerSupply
6+
>>> ps = PowerSupply()
7+
>>> ps.pv1.voltage = 4.5
8+
>>> ps.pv1.voltage
9+
4.499389499389499
10+
11+
>>> ps.pcs.current = 2e-3
12+
>>> ps.pcs.current
13+
0.00200014652014652
14+
"""
15+
import numpy as np
16+
17+
from PSL.i2c import I2CSlave
18+
from PSL.packet_handler import Handler
19+
20+
21+
class PowerSupply:
22+
"""Control the PSLab's programmable voltage and current sources.
23+
24+
An instance of PowerSupply controls three programmable voltage sources on
25+
pins PV1, PV2, and PV3, as well as a programmable current source on pin
26+
PCS. The voltage/current on each source can be set via the voltage/current
27+
properties of each source.
28+
29+
Parameters
30+
----------
31+
device : PSL.packet_handler.Handler
32+
Serial connection with which to communicate with the device. A new
33+
instance will be created automatically if not specified.
34+
35+
Attributes
36+
----------
37+
pv1 : VoltageSource
38+
Use this to set a voltage between -5 V and 5 V on pin PV1.
39+
pv2 : VoltageSource
40+
Use this to set a voltage between -3.3 V and 3.3 V on pin PV2.
41+
pv3 : VoltageSource
42+
Use this to set a voltage between 0 V and 3.3 V on pin PV3.
43+
pcs : CurrentSource
44+
Use this to output a current between 0 A and 3.3 mA on pin PCS. Subject
45+
to load resistance, see Notes.
46+
47+
Notes
48+
-----
49+
The maximum available current that can be output by the current source is
50+
dependent on load resistance:
51+
52+
I_max = 3.3 V / (1 kΩ + R_load)
53+
54+
For example, the maximum current that can be driven across a 100 Ω load is
55+
3.3 V / 1.1 kΩ = 3 mA. If the load is 10 kΩ, the maximum current is only
56+
3.3 V / 11 kΩ = 300µA.
57+
58+
Be careful to not set a current higher than available for a given load. If
59+
a current greater than the maximum for a certain load is requested, the
60+
actual current will instead be much smaller. For example, if a current of
61+
3 mA is requested when connected to a 1 kΩ load, the actual current will
62+
be only a few hundred µA instead of the maximum available 1.65 mA.
63+
"""
64+
65+
ADDRESS = 0x60
66+
67+
def __init__(self, device: Handler = None):
68+
self._device = device if device is not None else Handler()
69+
self._mcp4728 = I2CSlave(self.ADDRESS, self._device)
70+
self.pv1 = VoltageSource(self._mcp4728, "PV1")
71+
self.pv2 = VoltageSource(self._mcp4728, "PV2")
72+
self.pv3 = VoltageSource(self._mcp4728, "PV3")
73+
self.pcs = CurrentSource(self._mcp4728)
74+
75+
@property
76+
def _registers(self):
77+
"""Return the contents of the MCP4728's input registers and EEPROM."""
78+
return self._mcp4728.read(24)
79+
80+
81+
class _Source:
82+
RANGE = {
83+
"PV1": (-5, 5),
84+
"PV2": (-3.3, 3.3),
85+
"PV3": (0, 3.3),
86+
"PCS": (3.3e-3, 0),
87+
}
88+
CHANNEL_NUMBER = {
89+
"PV1": 3,
90+
"PV2": 2,
91+
"PV3": 1,
92+
"PCS": 0,
93+
}
94+
RESOLUTION = 2 ** 12 - 1
95+
MULTI_WRITE = 0b01000000
96+
97+
def __init__(self, mcp4728: I2CSlave, name: str):
98+
self._mcp4728 = mcp4728
99+
self.name = name
100+
self.channel_number = self.CHANNEL_NUMBER[self.name]
101+
slope = self.RANGE[self.name][1] - self.RANGE[self.name][0]
102+
intercept = self.RANGE[self.name][0]
103+
self._unscale = np.poly1d(
104+
[self.RESOLUTION / slope, -self.RESOLUTION * intercept / slope]
105+
)
106+
self._scale = np.poly1d([slope / self.RESOLUTION, intercept])
107+
108+
def unscale(self, current: float):
109+
return int(round(self._unscale(current)))
110+
111+
def scale(self, raw: int):
112+
return self._scale(raw)
113+
114+
def _multi_write(self, raw: int):
115+
channel_select = self.channel_number << 1
116+
command_byte = self.MULTI_WRITE | channel_select
117+
data_byte1 = (raw >> 8) & 0x0F
118+
data_byte2 = raw & 0xFF
119+
self._mcp4728.write([data_byte1, data_byte2], register_address=command_byte)
120+
121+
122+
class VoltageSource(_Source):
123+
"""Helper class for interfacing with PV1, PV2, and PV3."""
124+
125+
def __init__(self, mcp4728: I2CSlave, name: str):
126+
self._voltage = 0
127+
super().__init__(mcp4728, name)
128+
129+
@property
130+
def voltage(self):
131+
"""float: Most recent voltage set on PVx."""
132+
return self._voltage
133+
134+
@voltage.setter
135+
def voltage(self, value: float):
136+
raw = self.unscale(value)
137+
raw = int(np.clip(raw, 0, self.RESOLUTION))
138+
self._multi_write(raw)
139+
self._voltage = self.scale(raw)
140+
141+
142+
class CurrentSource(_Source):
143+
"""Helper class for interfacing with PCS."""
144+
145+
def __init__(self, mcp4728: I2CSlave):
146+
self._current = 0
147+
super().__init__(mcp4728, "PCS")
148+
149+
@property
150+
def current(self):
151+
"""float: Most recent current value set on PCS."""
152+
return self._current
153+
154+
@current.setter
155+
def current(self, value: float):
156+
raw = 0 if value == 0 else self.unscale(value)
157+
raw = int(np.clip(raw, 0, self.RESOLUTION))
158+
self._multi_write(raw)
159+
self._current = self.scale(raw)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy