The Raspberry PI power input control boards arrived and I assembled one and tested it. I built a wiring harness for the fan and for the temperature sensors and attached the temperature sensors to the heatsink. I built the final channel and put it in the case.
Also control interface got re-oriented and got a much needed speed up, I figured out how to start the software when the operating system boots and I built some code to automatically calibrate each channel and store the calibration values in EEPROM.
Raspberry Pi Power Control Board
So the power control board came back and looked pretty good overall. One problem was that the holes for some of the connectors were a bit tight. I tried slightly reaming out the holes but stuffed up one board in the process. Then it occurred that I could just file down the pins of the connector and this worked really well.
Oddly the female header connector for interfacing with the Raspberry PI isn't stocked at my local electronics store and so I had to order one from RS. This one connector however cost $5AUD which is totally silly.
I built a small board for the LED and power switch. The idea is the board is mounted on stand-offs that are just tall enough for the switch to protrude through the front panel. The problem was that there wasn't enough space for the connector as it has to mount on the component side (same side as the switch and LED) as it is a single sided PCB. To get around this I just soldered wires directly to the board and hot-glued them for mechanical strength. I screwed standoffs onto the board and hot-glued this to the back of the front panel. This works Ok overall.
Otherwise everything fitted nicely and worked very much as the proto-type did. Pretty happy with it. Only thing is the LEDs are a bit bright.
Temperature Sensors
In hindsight I really wish that I had integrated temperature sensing into each PSU channel and read the temperature via the channel interface.
Instead I built a wiring harness for the temperature sensors that basically wires them all in series. The termination resistor for the temperature sensor's 1-wire bus is on the Pi Power board so this is pretty simple.
The not-so-nice part is that then I have to mount them on the heatsink which involved hot-glue and a lot of cursing. What is worse is that they don't make very good thermal contact so the readings aren't the best (the temperature readings are much lower than what I get with a probe or non-contact thermometer). Still it will do for fan control purposes.
The reading rate of the sensor is quite low. I integrated this into the software by having it only read the temperature every few updates. I added a simple algorithm for fan control that basically ramps up the fan when the temperature goes over 30C. The fan hits full speed at 40C.
The reading rate of the sensor is quite low. I integrated this into the software by having it only read the temperature every few updates. I added a simple algorithm for fan control that basically ramps up the fan when the temperature goes over 30C. The fan hits full speed at 40C.
Starting the GUI
I didn't realize that Kivy doesn't actually run under X-Windows but basically directly accesses the frame-buffer. As a result I can use raspi-config to change the boot options and not start X-Windows. Now when I run the Kivy application from an ssh session is starts as before and works fine.
I created another daemon script (see previous post) for starting the power GUI. One change I had to make was that for some reason if I run the app as root the app will run but it doesn't respond to user input. A small change to the script allows it to run as the pi user and we are all good.
Now the Raspberry PI will boot and then will go straight into the power supply application.
Here is the daemon script (which again was derived from a template).
#!/bin/bash
### BEGIN INIT INFO
# Provides: labpower-gui
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Starts the Lab power supply GUI
# Description: Starts the Lab power supply GUI
### END INIT INFO
# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=/home/pi/LabPSU/LabPowerSupplyCtrl
DAEMON=$DIR/MainWindow.py
DAEMON_NAME=labpower-gui
# Add any command line options for your daemon here
DAEMON_OPTS=""
# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=pi
# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid
. /lib/lsb/init-functions
do_start () {
log_daemon_msg "Starting system $DAEMON_NAME daemon"
start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS
log_end_msg $?
}
do_stop () {
log_daemon_msg "Stopping system $DAEMON_NAME daemon"
start-stop-daemon --stop --pidfile $PIDFILE --retry 10
log_end_msg $?
}
case "$1" in
start|stop)
do_${1}
;;
restart|reload|force-reload)
do_stop
do_start
;;
status)
status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
;;
*)
echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
exit 1
;;
esac
exit 0
Ninety Degree Shift
The original GUI design was based around having the outputs for each channel below the screen. With the big 7" display there isn't enough space below to do this so instead the outputs are beside the screen. So to make this more usable I re-oriented the display to be in rows instead of columns. This way the details of each channel roughly lines up with the connector. The GUI is still a work in progress. Later I plan to add graphing and maybe a virtual knob to adjust voltage/current.
Speedup
Now that I have three channels running I found the GUI painful to use. The problem is that when you hit the voltage/current set buttons it can take quite a bit for the dialog to appear. This is because the code is sequentially cycling through and reading back the voltage/current/status from each channel and as the interface to the channels is relatively slow (115200 baud) this takes time. If you hit a button at the same time the code is running through some updates you have to wait for them to complete before it will respond to the button press.
I thought about how to speed this up and I thought perhaps I could increase the baud rate or even create a new 'uber' command that fetched all the relevant details back within a single exchange. The problem is that even if I do this the ADC is relatively slow and so the command will take some time to complete. I don't need it to be super-fast but I just want the GUI to be responsive.
I found out that python has quite good support for threading out of the box. I modified the code that manages each power supply channel so that it kicks off a thread that constantly fetches the status in the background. The thread reads the status into a bunch of class members which can then be read by the GUI whenever it updates. This way the updates to the three channels can happen concurrently and the GUI is never tied up.
The problem then is that if the GUI requests a change (say to set the voltage or whatever) while the thread is running, the commands generated by the thread and by the GUI will clash. Instead when the GUI requests a change the power supply channel code puts the request into a queue. Each time the thread goes round to do an update it checks to see if there is a command to process and sends that to the channel first before fetching the status.
The problem then is that if the GUI requests a change (say to set the voltage or whatever) while the thread is running, the commands generated by the thread and by the GUI will clash. Instead when the GUI requests a change the power supply channel code puts the request into a queue. Each time the thread goes round to do an update it checks to see if there is a command to process and sends that to the channel first before fetching the status.
To make accessing the status held by the power supply channels thread safe I created a small class that contains a mutex and which holds a copy of a variable. It has a setter and a getter method and it copies the value passed in and returns a copy on the way out. It locks the mutex before accessing the data in both directions to avoid problems with concurrent writes and reads.
I also changed the channel code so that when a dialog comes up it stops all the updates (not just the updates for the channel being commanded).
So now the dialogs still come up a little slow but when they do come up the buttons are responsive. The update speed of the GUI is fine and overall the GUI now works quite well.
I also changed the channel code so that when a dialog comes up it stops all the updates (not just the updates for the channel being commanded).
So now the dialogs still come up a little slow but when they do come up the buttons are responsive. The update speed of the GUI is fine and overall the GUI now works quite well.
Auto-Calibration
The tables of points used to calibrate the ADC/DAC readings was stored in constants within the code. I generated these constants using a small python script that commanded the supply to go through the full range for voltage/current outputs while reading back values from a LAN attached multimeter.
The problem with this is that each channel needs a different table of values and I don't really like having different firmware images per board.
The logical thing is to update the firmware so the table of points is stored in EEPROM and provide functions to read/write these values. The python code for stepping through the output range can then be integrated into the Kivy GUI.
This turned out to be a bigger job than I hoped. I had to:
The problem with this is that each channel needs a different table of values and I don't really like having different firmware images per board.
The logical thing is to update the firmware so the table of points is stored in EEPROM and provide functions to read/write these values. The python code for stepping through the output range can then be integrated into the Kivy GUI.
This turned out to be a bigger job than I hoped. I had to:
- Create a class for reading the linearizer data from EEPROM
- Update the Linearizer code so it can be setup after construction.
- Add a command to set the number of points and to set each point in the linearizer table for the Voltage ADC/DAC and Current ADC/DAC
- Modify the Kivy GUI to pause everything, allow the GUI to directly (not via the thread) command the channel to move through each point and to communicate with the multimeter to read-back the voltage. The GUI needed a progress bar display and needed to allow the user to enter the hostname/IP of the DMM.
Running out of RAM on the Microcontroller
The first thing that happened was I loaded the code into a channel and it basically was non responsive. After some poking and the addition of text debugging I figured out that the firmware ran out of RAM (the ATMEGA328P only has 2K)
I was able to figure this out by using this bit of code for working out how much free RAM there is based on the stack address
int freeRam ()
{
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
int freeRam ()
{
extern int __heap_start, *__brkval;
int v;
return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval);
}
printf("Free ram is %d\r\n",freeRam());
This was I could pepper the code to see where the RAM was being consumed and how much. Because of the way the interpreter works, most of the RAM is allocated when the main object is created.My biggest problem was that in addition to the tables for each of the ADC/DAC linearizers I had static constants containing default tables (in case the EEPROM is blank). Static constants end up using both EEPROM *and* RAM. The compiler allocates RAM for the constant, reads it from EEPROM and copies it into RAM. I changed the defaults to be dumb one point tables and this brought the usage down considerably.
The other thing was I included a pointer to the LabPSU object in every command. Assuming pointers are 4 bytes, as there are 26 commands this equates to quite a bit of RAM wasted. I changed the code so the pointer is passed in during parsing and recovered loads of RAM.
The final change was I switched from 32 points to 16 points for the tables and this was enough for the code to run reliably.
Next Steps
There are a few things I'd like to add to this project but none of them are urgent. Furthermore I've been working on this for nearly two years and I feel like it is time to do something new for a while. So for now I am calling this complete although I might add some new features in the future.
The future list includes:
- Virtual onscreen knob for adjusting voltage/current
- In addition to displaying the current/voltage/power a graph mode where you can see a graph of the output current/power
- A curve tracer mode for plotting IV characteristics of LEDs etc
- Maybe a battery discharge simulator.
Here it is on my bench in action.
No comments:
Post a Comment