15 Jul 2022 - tsp
Last update 15 Jul 2022
13 mins
TL;DR: The protocol is simple, no start and termination of commands, plain ASCII over a serial port or the USB to serial bridge. The code presented is available on GitHub
So after having used them at work for a few months I just had to get my own KA3005P bench-top lab power supply. Up until then I used some dumb (not controllable via serial port / USB / Ethernet) cheap bench-top power supplies that covered the same range as well as some custom home built supplies - and also still have the plan to build some switchmode supplies based on the TL494 by myself, especially for the high voltage regime. But since those supplies are rather affordable and their control is done simple via RS232 or an integrated USB to serial converter this had been the easier way to go for simple non specialized stuff.
The next step was to look into how those devices are controlled. There is some documentation available:
Since the protocol is really pretty simple I started to implement my own library in ANSI C and in Python (I tend to use C99 for serious stuff and Python for playing around). To avoid designing APIs over and over again I also started a small collection of Python base classes for various laboratory devices such as power supplies, function generators, etc. - this will grow for sure - from which I then derive the implementations themselves so every new device will use exactly the same API as similar ones which allows one to simply swap devices.
The main reasons for a custom own implementation are the same as usual:
The KA3005P is a standard linear power supply that support setting a target voltage as well as a current limit. It would also allow one to store different settings at four memory banks (I don’t make use of that - it’s mainly useful when you’re controlling the supply manually) and in addition to normal operation that one knows from other lab supplies it also support overcurrent and overvoltage “protection” modes in which triggering the voltage or current limit (constant voltage or constant current mode) disabled the output again which might be useful under some circumstances especially when testing circuits.
The KA3005P support computer or microcontroller control via two different interfaces
though only one can be active at the same time - there is a main serial port that
runs on RS232 levels as well as an integrated USB to serial adapter that runs as
a full speed (12 Mbps) USB device with the identification “Nuvoton USB Virtual COM”.
It’s vendor ID is 0x0416
, the product ID 0x5011
and announces itself
as a communication device (CDC ACM). Under FreeBSD the stock kernel uses the umodem
driver to supply an tty
device one can use to communicate with the power supply
when using the USB port.
On both interfaces the settings are:
19200
In addition I usually define a short timeout of a second at most.
The power supply does not use any kind of robust serial communication such as a start
pattern, a checksum or an end pattern. It seems to do simple pattern matching
on the commands sent and usually one is required to know the length of the
expected response to robustly decode it. Despite this the protocol seems to
orient itself somewhat at SCPI (which also differs from device to device) there
are no line endings, it’s really simple to implement using write
and read
from the serial port using pyserial
or standard Unix file access methods.
The following functions that I know of are supported by the KA3005P:
Command | Response length (bytes) | Description |
---|---|---|
*IDN? |
30? | Returns the type, software version and serial number of the power supply (ex.: KORAD KA3005P V5.8 SN:YYYYYYYY ) |
OCP0 , OCP1 |
0 | Disable or enable the over current protection. When this is enabled and the current exceeds the set value the power supply simply disables the output |
OVP0 , OVP1 |
0 | Disable or enable the over voltage protection. In case the voltage exceeds the set value the power supply disabled the output |
OUT0 , OUT1 |
0 | Disable or enable the output |
VSET1:XX.XX |
0 | Sets the voltage to the specified value (XX.XX ). The value always has 2 digits before and two after the decimal dot in ASCII notation |
ISET1:X.XXX |
0 | Sets the current to the specified value (X.XXX ). The value consists of ASCII values and a decimal dot at the second position |
STATUS? |
1 | Query the single status byte. This is interpreted binary (see below) |
VSET1? |
5 | Query the current voltage setting. The response are 2 ASCII digits, one decimal dot and 2 ASCII digits after the decimal dot |
ISET1? |
5 | Query the current current setting. The response has the same format of 4 ASCII digits with a decimal dot at the second position |
VOUT1? |
5 | Request a measurement of the actual voltage on the output. This has the same format XX.XX as setting and getting the set value |
IOUT1? |
5 | Request a measurement of the actual current on the output. This has the same format X.XXX as setting and getting the set value |
The status flags known to me are:
Bit | Meaning | Comments |
---|---|---|
7 | Overvoltage protection enabled (1) or disabled (0) | Seems to be somewhat unreliable |
6 | Output enabled (1) or disabled (0) | |
5 | ||
4 | Overcurrent protection enabled (1) or disabled (0) | Seems to be somewhat unreliable |
3 | ||
2 | ||
1 | ||
0 | Constant current mode (1) or constant voltage mode (0) |
TL;DR: The code is spread over two repositories:
First I decided on the abstraction for power-supplies that I’d like to have and implemented those in my pylabdevs library as PowerSupply base class. The public functions that will later be used by applications are:
Method | Description |
---|---|
__init__ |
See below, this configured the behavior of the power supply and it’s capabilities |
setChannelEnable(enable, channel=1) |
Switches the output on or off |
setVoltage(voltage, channel=1) |
Sets the desired target voltage |
setCurrent(current, channel=1) |
Sets the desired target current |
getVoltage(channel=1) |
Returns a tuple with the measured (or None) and the set voltage |
getCurrent(channel=1) |
Returns a tuple with the measured (or None) and the set current |
off() |
Disables all outputs |
getLimitMode(channel=1) |
Returns either PowerSupplyLimit.VOLTAGE or PowerSupplyLimit.CURRENT when output is enabled for constant voltage and constant current mode or PowerSupplyLimit.NONE in case the output is disabled |
The init function is rather complex and allows one to specify the capabilities of the power supply:
nChannels
is an integer that tells the class how many channels are
available. It’s assumed that all channels have the same capabilitiesvrange
and arange
specify voltage and current range, prange
the supported power range. All three values require a tuple specifying minimum,
maximum and supported step size (assumed to be constant over the full range)capableVLimit
and capableALimit
to indicate if a power
supply can work in constant voltage or constant current mode.capableMeasureV
and capableMeasureA
indicate if a powersupply is
able to measure the actual voltage and currentcapableOnOff
indicates if the powersupply can be enabled and disabledAll methods verify the supplied parameters already in the base class and are not
overridden by the actual implementations. Note that voltage and current setters
also verify if the maximum power range is exceeded or not with the desired setting
and will raise an ValueError
in this case.
The actual communication will be done by a set of private routines that are
overridden by the actual classes and which do raise an NotImplementedError()
in case they are called on the base class. Those are:
Implementation method | Description |
---|---|
_setChannelEnable(enable, channel) |
Enables or disabled the output of the channel |
_setVoltage(voltage, channel) |
Try to set (and verify the setting of) the target voltage of the given channel |
_setCurrent(current, channel) |
Try to set (and verify setting of) the current limit of the given channel |
_getVoltage(channel) |
Measure the actual voltage on the given channel |
_getCurrent(channel) |
Measure the actual current on the given channel |
_off() |
Disable all outputs at once |
_getLimitMode(channel) |
Return in which limit mode (voltage, current or none) the supply currently runs |
_isConnected() |
Returns True when the device is currently connected |
The setting methods usually should set a value and then read back the current
setting from the device if supported to make sure it has been written correctly.
It’s also assumed the implementations support using with
by supplying
an __enter__
and __exit__
routine. When exiting the context the power supply
should turn off all outputs - the same goes for terminating the process which is
usually realized using atexit
even though the base class also implements
calling _off()
in an atexit
routine itself.
In my own implementation in pyka3005p I’ve
implemented the KA3005PSerial
class that inherits from PowerSupply
. There are two methods that handle the
serial communication internally:
_sendCommand
is used to transmit the (ASCII string) command supplied without
expecting any reply from the device._sendCommandReply(cmd, replyLen = None)
is used to transmit the (ASCII string)
command supplied and expects the device to respond. The replyLen
parameter
specifies either a positive integer of the number of bytes expected (in case not
enough bytes are received in the timeout window an IOError
is raised). In
case replyLen
is set to None
the method would expect a NULL terminated
response which is actually not used in the implementation any more. Whenever
a negative integer is supplied the method reads response bytes and assumes that
a timeout indicates an end of the message - this is used for the identify response
to the *IDN?
command only.In addition an internal __close
method is implemented and registered as
an atexit
handler to tear down the serial port in case of an abnormal termination
of in case a normal context exit is triggered.
This now allows pretty simple usage of the power supply using context routines or an imperative style:
from pyka3005p.ka3005pserial import KA3005PSerial
with KA3005PSerial("/dev/ttyU0") as psu:
psu.setVoltage(5.00)
psu.setCurrent(0.510)
psu.setChannelEnable(True)
measV, setV = psu.getVoltage()
measA, setA = psu.getCurrent()
print("Measured voltage {} V (set {} V)".format(measV, setV))
print("Measured current {} A (set {} A)".format(measA, setA))
sleep(10)
psu.setChannelEnable(False)
Alternatively one can use the imperative style without context routines which is particularly useful for more complex scripts or Jupyter notebooks:
from pyka3005p.ka3005pserial import KA3005PSerial
psu = KA3005PSerial("/dev/ttyU0")
# One has to call connect ...
psu.connect()
psu.setVoltage(5.00)
psu.setCurrent(0.510)
psu.setChannelEnable(True)
measV, setV = psu.getVoltage()
measA, setA = psu.getCurrent()
print("Measured voltage {} V (set {} V)".format(measV, setV))
print("Measured current {} A (set {} A)".format(measA, setA))
sleep(10)
psu.setChannelEnable(False)
# And in the end disconnect
psu.disconnect()
There are of course many applications especially in physics experiments such as setting magnetic fields, controlling heating elements, measuring pressure using the power dissipated in a Pirani gauge, controlling laser power and frequency, etc.
A simple application of this library is just recording constant current curves
when characterizing electronic parts or systems. In this case one can simply
scan the voltage until the output current reaches a desired limit and record
the measured current at each point. In addition this can of course be repeated
more than once to reduce measurement error on noisy systems but since the
supply will only return with a resolution of 10 mA this won’t be necessary
in many cases. A simple example script that does exactly that and plots
the result with matplotlib
is supplied in the examples
directory
of the pyka3005p
repository.
A simple example measurement of a 1N4001
diode in forward direction up to
2A of current (exceeding the specification of 1A) is shown in the graph below:
A simple example on how to capture such data is available as GitHub GIST
This article is tagged:
Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)
This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/