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

9 files changed

+240
-214
lines changed

Diff for: PSL/Peripherals.py

-122
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

Diff for: PSL/power_supply.py

+159
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)