Falling down a lab automation rabbit hole.
To make the experience fit your profile, pick a username and tell us what interests you.
We found and based on your interests.
A quick update on the current state of this homebrew python project. The project is still very much a work in progress, but it’s polished enough that I figured I’d make it public. My long-term goal is to incorporate all the equipment from the office, my colleagues, and my own lab. Also, I’d like to add the module to the Python Package Index (PyPI). I’m already part way there, all my equipment has been added at least! Here’s a demo of what the thing can do right now….
The general file structure for the project is shown below (excluding some directories that aren’t super important right now).
LABASSISTANT
├── ABC
│ ├── DMM.py
│ ├── ELOAD.py
│ ├── PSU.py
│ └──SCOPE.py
├── config
│ ├── Digital_Multimeter
│ │ ├──siglent_sdm3055.py
│ │ └──fluke_8840a.py
│ ├── Electronic_Load
│ │ └──siglent_sdl1020xe.py
│ ├── Function_Generator
│ ├── Oscilloscope
│ └── Power_Supply
├── enums
│ ├── eload_enum.py
│ ├── generic_enum.py
│ └── scope_enum.py
├── errors.py
├── generic_device.py
├── lab_assistant.py
└── registry.py
In terms of inheritance, starting from the children, we have devices (stored in the config folder... maybe I need to rename that -_- ). For example, fluke_8840A. This device inherits from the DMM class which itself inherits from GenericDevice. In general, any device that inherits from the DMM class is compatible, but may be missing certain features. For example, SDM3055 can measure capacitance but 8840A can’t. In cases like this you’d get an error if you try to call FlukeDMM.measure(MeasureType.Capacitance).
To setup a device you just call one of LabAssistants setup_xxx methods... for example.
my_scope = LabAssistant.setup_scope(resource="TCPIP::192.168.1.11::INSTR")
my___psu = LabAssistant.setup_psu(resource="TCPIP::192.168.1.12::INSTR")
my_eload = LabAssistant.setup_eload(resource="TCPIP::192.168.1.13::INSTR")
my___dmm = LabAssistant.setup_dmm(resource="GPIB0::1::INSTR")
The script will query the device at the specified port to determine the make and model, and dynamically load the relevant class. However, if you’d rather a more controlled approach you could use the “Forced_Driver” argument to tell the script exactly what device to expect. In the example below, if a fluke 8840a were not found at that resource, an error would be risen.
my___dmm = LabAssistant.setup_dmm(resource="GPIB0::1::INSTR", Forced_Driver = "fluke_8840a")
Once the connection is established, we can set the measurement type and read values from the dmm. Currently only single sample modes are supported, but I plan to add more configurable measurement methods in the future.
my___dmm.measure(MeasureType.VOLTAGE)
It's that easy!
I'll record some video demo's of the other classes later this week. If you're interested in learning more, you can poke around the public repo.
I recently acquired a new Fluke 8840A DMM. Sadly, I learned its unconventional command formatting would require some significant tweaks to my existing codebase; moreover, these tweaks would bloat with every new device I have to implement. At least the new devices that have some unconventional setup requirements. Hindsight is 20/20, so I decided to bulldoze my entire repo and start again from scratch. This time trying to emphasis an instrument-agnostic system.
As a reminder I’d like this module to support common instruments such as power supplies, e-loads, oscilloscopes, DMM’s and function generators for starters. I’d like to add some more exotic instruments at a later date, but these are my targets for first release.
To create an instrument-agnostic system, requires some amount of upfront effort to add support for each instrument. This configuration will come in the form of small json config files for each instrument. Each instrument will have several basic methods that should be filled out, dependent on what type of instrument it is. Here’s an example json file for my SPD1168 power supply. The user must fill out the string to be sent to the device to accomplish each function. Some functions may not be supported such as “set_remote_sense”. In cases like this the command can be left blank. On my current version of the project, this will raise a warning, but won’t halt the process.
{
"Manufacturer": "Siglent",
"DeviceType": "Power Supply",
"channels": 1,
"init": [""],
"reset": ["*RST"],
"operation_complete":["*OPC?"],
"set_output_en": ["OUTP CH{channel},ON"],
"set_output_dis": ["OUTP CH{channel},OFF"],
"set_voltage": ["CH{channel}:VOLT {value}"],
"set_current": ["CH{channel}:CURR {value}"],
"set_remote_sense": [""],
"get_voltage":["CH{channel}:VOLT?"],
"get_current":["CH{channel}:CURR?"],
"meas_voltage": ["MEAS:VOLT? CH{channel}"],
"meas_current": ["MEAS:CURR? CH{channel}"],
"meas_power": ["MEAS:POWE? CH{channel}"]
}
Here's a short example of the eload class test script. I’ll be refining the codebase some more this weekend and hopefully making in public (though still prerelease).
# ================ Electronic Load Test ================
elif( device_type == DeviceType.ELOAD):
myEload = ELOAD(USER_Eload_ip, ConnectionType.Ethernet)
print("====================================================\n"
"==================== ELOAD TEST ====================\n"
"====================================================\n")
# Toggling the output - errr. I guesss input in this case ;) -
print("Enable Eload Output")
myEload.set_output_dis(Channel.CH1)
sleep(0.5)
myEload.set_output_en(Channel.CH1)
sleep(0.5)
# Test Constant Current
test_load = round(random(),2)
print("\nTest CC mode @"+str(test_load)+"A")
myEload.set_mode_type(EloadMode.CC, Channel.CH1)
myEload.set_load_value(test_load, Channel.CH1)
# Check if set
set_value = myEload.get_load_value(Channel.CH1)
if(set_value == test_load): print("PASS")
else: print("FAIL -> Read "+ str(set_value))
# Test Constant Resistance
test_load = round(random()*100,2)
print("\nTest CR mode @"+str(test_load)+" Ohm")
myEload.set_mode_type(EloadMode.CR, Channel.CH1)
myEload.set_load_value(test_load, Channel.CH1)
set_value = myEload.get_load_value(Channel.CH1)
if(set_value == test_load): print("PASS")
else: print("FAIL -> Read "+ str(set_value))
# Test Constant Power
test_load = round(random(),2)
print("\nTest CP mode @"+str(test_load)+"W")
myEload.set_mode_type(EloadMode.CP, Channel.CH1)
myEload.set_load_value(test_load, Channel.CH1)
set_value = myEload.get_load_value(Channel.CH1)
if(set_value == test_load): print("PASS")
else: print("FAIL -> Read "+ str(set_value))
# Test Constant Power
test_load = round(random(),2)
print("\nTest CV mode @"+str(test_load)+"V")
myEload.set_mode_type(EloadMode.CV, Channel.CH1)
myEload.set_load_value(test_load, Channel.CH1)
set_value = myEload.get_load_value(Channel.CH1)
if(set_value ==...
Read more »
Overview
As mentioned in my last log, I had an issue where I “needed” to explain why a current mirror wasn’t working as expected. I thought I had helped solve a colleague’s issue, but it turned out to a be red herring… now their current mirror was using Onsemi FMBA06 (for those playing along at home) and still performing terribly.
Two birds one stone. I decided I’d try to root cause the issue and learn lab automation at the same time. I knew my equipment could be programmed but wasn’t sure how. Luckily, I quickly found PyVisa1, and spun up a BJT Plotter2 over a weekend.
Note 1 - If you have some new-ish lab equipment likely supports SCPI… I strongly recommend you give this python package a try. Its freaking phenomenal!
Note 2 – Yes, I’m aware you could accomplish similar results with a function generator and an oscilloscope in XY mode… let me cook!
Setup
The test setup is shown below… I should REALLY replace my Oscilloscope with a bench DMM (insert “if I had one meme”). The function generator is used as a DC supply capable of 1mV steps. I have to use a fairly large base resistor to squeeze out any accuracy I can from my scope. The scope then measures the current into the base, as well as the voltage Vce, swept by the PSU.
Scripting - Set Ib & Sweep VCE
The script should be as easy as (1) set Ib, then (2) sweep Vce? And yup it’s that easy. Shown below is the main for loop of the BJT Plotter script.
To set the base current I use the Set_Ibase function shown below. It first assumes Vdrv = Ib*Rb-Vbe, then it sets the function generator to output Vdrv, and measures the true Vbe. Vdrv can then be recalculated, and the process can be repeated. I found that this method quickly converged to its final value, but since Vbe is so small it’s difficult to converge to the CORRECT value.
The second method is to assume Vbe is constant, and then focus on the desired change in base current. This way we’re not dealing with tiny changes in voltages (below 1mV in the case of Vbe), and I can focus on the somewhat large voltage across Rbase.
Here’s an example of the 2-stage algorithm working. Notice that iteration 2 and 3 measured IDENTICAL Vbe. I need a much more accurate/precise instruments to use method 1 by itself.
Final Result
Sweeping the part revealed the following curves… Gross! At higher currents the knee of the saturation region stretches out much further than I expected. We can imagine the two transistors in the current mirror as the X’s on the 500uA curve. The left X would be the load setting BJT, and the right X would be the sinking/sourcing BJT. The left X is locked to Vbe(on), but the right X will see Vce varied with load. Clearly this current mirror won’t work great, and it doesn’t!
A quick sanity showing the BJT plotter on a well-documented part (2n3904). Curves look good!
TLDR - Onsemi FMBA06 makes for a poor current mirror at my operating point.
Why? Fair question… this plotter was the fallout of 2 converging topics. (1) Recently I was debugging a current mirror and was frustrated that there were no IV curves on the BJT’s datasheet, and (2) I’ve been meaning to find an excuse to start automating my lab.
The Humble Current Mirror…
As a reminder the basic current mirror that was drilled into my head through university is shown below. The key idea of this design is that the base-emitter voltage drop is mirrored by both transistors. If the two parts are well matched this should lead to identical base current, and thus similar collector current (Ic = B*Ib). The current across Rload is set by changing the value of Rbias…. Current = (Vcc-0.6)/Rbias.
Current Mirror Woes…
While debugging a circuit at work, I started to think about this design a bit more critically… and came to the realization “WOW the generic current mirror sucks!” Or at least, you NEED to look very closely at the BJT pair you’re using. Let’s go over the common issues with this circuit, hopefully when I inevitably run into another of these in the future, I’ll know the red flags.
First and most common issue is BJT matching. This issue can be explained by (Ic = B*Ib)… since Vbe should be mirrored, we’ll assume Ib is too. In this case, Ic will be directly proportional to the gain beta. Any error in beta between these two transistors result in an error of our expected collected current. If we look at the datasheet for a jellybean npn BJT we see a wide spec for min/max gain (100/300).
Next issue, the early effect! The early effect shows itself as a gradual increase in collector current as Vce is increased (see below). We previously mentioned our leftmost transistor has its Vce “locked” around 1 diode drop, but the right one is meant to maintain its current regardless of Vce… Clearly, looking at the graph below, collector current is still a function of collector-emitter voltage!!! If we want our current mirror to function well, we want the Early Voltage "Va" to be as large as possible. Otherwise, we’ll see a sizeable error that is dependent on Vce.
Last issue, is the operating point of the leftmost transistor. We know that Vce will be locked near 0.6V, what happens if the left transistor is in saturation while the right is active? See an example below (taken from Nexperia BCM56DSF)… If I used a BCM56DSF based current mirror set to 50mA, it seems like one part will be operating in the “knee region” (errr saturation) while the other would be comfortably in the active region. We expect the two set points to be along the same Ib “curve” of this BJT characteristic, see below. We can avoid this problem, with our transistor (low saturation voltage) & setpoint.
Note: I couldn't find any mention of this "issue" with current mirrors online, so maybe I'm missing something here... take my last gripe with a pinch of salt
TLDR - Don't use a half baked Current Mirror in your circuit.
Create an account to leave a comment. Already have an account? Log In.
Become a member to follow this project and never miss any updates