In Test and Measurement Automation projects, we often have to make numerous, quick measurements for test points on a device being tested, which we make with multiplexers. For a recent project, we needed to measure multiple AC voltages on a Mobile Energy Storage System.
We chose multiplexers that are rated for high voltages, can carry a decent amount current, and are rated by their manufacturer to have their relays switched on and off plenty of times. These are mechanical systems, however, and, if you build enough test systems with multiplexers that constantly switch relays on and off, one of them will eventually fail. Any part of a factory line system failing means downtime, which can lead to losses.
The best approach to test systems that last and deliver value is to expect that they may fail at some point. At DMC, we design tools to delay and diagnose the inevitable as opposed to ignoring it.
To this end, we deployed our project with a diagnostic sequence and additional hardware that the client could use periodically to detect wiring and relay faults so we can fix them. There are several ways in which this idea can be applied.
Take the example of testing a standard 120V AC outlet: we need to check that we see ~120V between live and neutral as well as live and ground but ~0V between neutral and ground.
In our case, we mapped DMM and each of the test points to multiplexer coordinate “paths” and those paths to human-readable names through our custom “MUX Manager” library. This enabled us to control relays with Python lines like these:
mux_manager.get_pin_path(<Pin Name>, <Rail>)
mux_manager.set_pin(<Pin Path>, <New State>)
mux_manager.read_pin(<Pin Path>)
mux_manager.clear_all()
If we wrote a generic method in a test class to measure the voltage between two pins, a first attempt would look something like this:
class VoltageTest:
…
def test_voltage(self, pin_a, pin_b, expected, tolerance):
self.mux_manager.clear_all()
path_a = self.mux_manager.get_pin_path(pin_a, Rail.POSITIVE)
path_b = self.mux_manager.get_pin_path(pin_b, Rail.NEGATIVE)
self.mux_manager.set_pin(path_a, True)
self.mux_manager.set_pin(self.dmm_positive_path, True)
self.mux_manager.set_pin(path_b, True)
self.mux_manager.set_pin(self.dmm_negative_path, True)
measurement = self.dmm.read_voltage()
# Clearing up connections
self.mux_manager.clear_all()
if (expected - tolerance) < measurement < (expected + tolerance):
return "PASS"
else:
return "FAIL"
We need to clear up the connections at the end of each function call to leave the multiplexer in a clean state so that calls to test_voltage can be rearranged in a test sequence by an engineer without worrying about what pins are previously enabled. On the other hand, when we imagine how such a method would be used, we start to see some redundancy. In testing the 3-pin outlet, we would have to enable and disable DMM pins 12 times and get similar inefficiencies with the pins being tested.The problem is compounded when this generic function is run hundreds of times in one sequence and that sequence is run hundreds of times. The likelihood of a single relay failing (and therefore risk of downtime) is made unnecessarily high by lazy programming.
Instead, we could use our knowledge about our system and the math abilities of Python to eliminate this redundancy with a method in the MUX Manager:
class MUXManager:
…
def masked_set_pins(self, pin_and_rail_list):
high_pins = set()
for pin_name in self.pin_names:
pin_path_positive = self.get_pin_path(pin_name, Rail.POSITIVE)
pin_path_negative = self.get_pin_path(pin_name, Rail.NEGATIVE)
if self.read_pin(pin_path_positive):
high_pins.add(pin_path_positive)
if self.read_pin(pin_path_negative):
high_pins.add(pin_path_negative)
need_to_be_high_pins = set()
for pin_name, rail in pin_and_rail_list:
pin_path = self.get_pin_path(pin_name, rail)
need_to_be_high_pins.add(pin_path)
need_to_make_low_pins = high_pins - need_to_be_high_pins
for path in need_to_make_low_pins:
self.set_pin(path, False)
need_to_make_high_pins = need_to_be_high_pins - high_pins
for path in need_to_make_high_pins:
self.set_pin(path, True)
By using set differences, we know which relays to turn off and on from a previous state and avoid any calls in the process that would be redundant.
This is not a perfect approach and requires knowledge of the system such as whether you would be performing any hot switching or there needs to be an order to the switch calls.
For our voltage tests, however, this approach was the right one and worked best for our client’s needs. We can rewrite our test_voltage method to incorporate this new method as follows:
class VoltageTest:
…
def test_voltage(self, pin_a: str, pin_b: str, expected, tolerance):
self.mux_manager.masked_set_high_pins([
(pin_a, Rail.POSITIVE),
("DMM+", Rail.POSITIVE),
(pin_b, Rail.NEGATIVE),
("DMM-", Rail.NEGATIVE),
])
measurement = self.dmm.read_voltage()
if (expected - tolerance) < measurement < (expected + tolerance):
return "PASS"
else:
return "FAIL"
Our method is more readable and efficient while reducing the relay switches per measurement. The animation given shows the decreased switching actions needed (from 24 down to 12) with this new approach — which is even more pronounced for outlets with more pins.
What I like most about this whole endeavor is that it confronts the fact that it illustrates how thoughtful engineering design can be exemplified in many ways — in hardware and software. By complicating how our software works, our hardware takes fewer steps and lives longer. Good multiplexing is, well, multiplex.
Learn more about DMC's Battery Pack and BMS Test Systems and contact us today for your next project.