Saturday, January 10, 2015

Power supply project - part 2

This is a follow-up to http://hackcorrelation.blogspot.de/2013/07/power-supply-project-part-1.html in which I was trying to fix a car battery charger but ended going down the 'feature creep' path.


It still contains two power supplies, one is 0-17V, the other one 7-25V. Unfortunately the input voltage is limited to 19V, which means the maximum output is lower than that. I would probably dump the transformer and put a 24V/2A HP printer power supply instead.

The additions are two sets of binding posts and a power switch, better noise handling (star ground) and a new firmware.

The top LCD display shows the voltage and current for the first power supply, with a nice 'clock' graph for the voltage and a thick horizontal bar graph for the current.
This is duplicated for the second supply.
The microcontroller also has a graph mode that can display any value over time, auto-scaled. Since the button to access that is on the back of the board I don't use it very often.

I would not tackle this project again without a real circuit board as the mess of wires makes it really hard to keep track of everything. Because of the continuously changing requirements I kept adding features that were not part of the initial plan:


Luckily I have had no loose connections, but did get into noise issues. Also, the thin wires meant that a lot of voltage is dropped when there are consumption peaks.
All the high-current paths are done with moderately gauged wires.
In the photo above everything is loose but now everything has been tied and grouped together, some wires were also twisted to reduce common noise.

Current sensing

I ditched the 0.36 ohm resistors for sensing current (two of them in parallel) as they required a lot of calibration, modified their values over time, had little precision and low precision.
Instead, I purchased two ACS712 modules over ebay for around 3$ each. These output a value centered over 2.5V which changes with 185mV/A. This means that their output value can increase when current flows one way and decreases when it goes the other way.
Since the Stellaris is limited to 3.3V, I chose to measure current supplied the 'wrong' way around, otherwise I would have been limited to (3.3-2.5)/0.185 = 4.324A. With this reversal I now get a measuring range of -4.32A to +17.83A.

Even with these modules I got a lot of noise, 200mV translates to 1.08A variance in reading:


These were ultimately traced to several ground loops that I inadvertently introduced while working without a clear block diagram.

One last picture of the mess, the blue module on the left is the ACS712, the red one on the right is the Stellaris Launchpad. Just behind the black binding post is the LM2596 supply.


Code

This is not production code. I would have moved some buttons to the front to allow calibration, made the backlight adjustable and move the microUSB connector outside the case to allow for easier programming. However it does the job and I don't intend adding more features.

Features that I wanted to have but dropped them:

Current and voltage protection modes

Have the uC disable the PSU which goes over a set threshold. Both supplies have disable pins which are really easy to use. Unfortunately I have to spend a lot of time adding either some potentiometers to set a value or rotary encoders. Also the screen layout needs to be changed to accommodate the new features.
Even then, the protection would only work when the uC has time to handle the task, which can mean from microseconds to milliseconds of delay.
Easier to do with a few comparators.

Remote control and readout

I've managed to get the nRF24L01 modules to work really well with the Launchpad and send/receive that to/from a Raspberry PI. However the real issue is writing the server-side code - mainly the design and coding part.

Source

This is not meant to be reused, but you might find some useful tricks:
- using a multi-tasking environment
- drawing dials and auto-scaling rolling graphs
- averaging
- calibration

 // Include application, user and local libraries
#include "Energia.h"

#include "inc/hw_memmap.h"
#include "inc/hw_types.h"
#include "driverlib/debug.h"
#include "driverlib/sysctl.h"
#include "driverlib/adc.h"

#include <Adafruit_GFX.h>
#include <Adafruit_PCD8544.h>
#include <Queue.h>

// pin 7 - Serial clock out (SCLK)
// pin 6 - Serial data out (DIN)
// pin 5 - Data/Command select (D/C)
// pin 4 - LCD chip select (CS)
// pin 3 - LCD reset (RST)
Adafruit_PCD8544 display = Adafruit_PCD8544(PC_5, PC_6, PC_7, PD_6, PD_7);

#define V1_ADC_COEF 0.00876072f
//#define V1_MAX_RANGE 28
#define V1_MAX_RANGE 17

#define V2_ADC_COEF 0.00885885f
//#define V2_MAX_RANGE 37
#define V2_MAX_RANGE 19

//#define I1_ADC_COEF 0.00143868f
// ACS712-51B sensor board - current readings are centred around 2.5V with a 185mv/A factor
#define I1_ADC_COEF ((3.3f / .185f) / 4096.0f)
#define I1_ADC_OFFSET (4096.0f / 3.306f * 2.49f)
#define I1_MAX_RANGE 5

#define I2_ADC_COEF ((3.3f / .185f) / 4096.0f)
#define I2_ADC_OFFSET (4096.0f / 3.306f * 2.5f)
#define I2_MAX_RANGE 5

#define RBUFFER_SIZE LCDWIDTH
#define PIXEL_DOUBLING 0
#define PIXELS_INSTEAD_OF_LINES 0

const int v1adc = PD_0;
const int i1adc = PD_3;
const int v2adc = PD_2;
const int i2adc = PD_1;

//measurements
uint32_t adcV1;
uint32_t adcV1Readout;
uint32_t adcI1;
uint32_t adcI1Readout;
uint32_t adcV2;
uint32_t adcV2Readout;
uint32_t adcI2;
uint32_t adcI2Readout;
uint32_t temperature;

int32_t voltages[RBUFFER_SIZE];

volatile char currentScreen = 0;

//tasks
int takeMeasurements(unsigned long now);
int readTemp(unsigned long now);
int displayUI(unsigned long now);
int readButtons(unsigned long now);


uint32_t analogReadAveraged(uint8_t pin)
{
  uint8_t port = digitalPinToPort(pin);
  uint16_t value[1];
  uint32_t channel = digitalPinToADCIn(pin);
  if (pin == NOT_ON_ADC) { //invalid ADC pin
    return 0;
  }
  ROM_SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0);
  SysCtlADCSpeedSet(SYSCTL_ADCSPEED_125KSPS);
  ROM_GPIOPinTypeADC((uint32_t) portBASERegister(port), digitalPinToBitMask(pin));
  ROM_ADCSequenceConfigure(ADC0_BASE, 1, ADC_TRIGGER_PROCESSOR, 0);
  ROM_ADCSequenceStepConfigure(ADC0_BASE, 1, 0, channel | ADC_CTL_IE | ADC_CTL_END);
  //ROM_ADCHardwareOversampleConfigure(ADC0_BASE, 64);
  ROM_ADCSequenceEnable(ADC0_BASE, 1);

  ROM_ADCIntClear(ADC0_BASE, 1);
  ROM_ADCProcessorTrigger(ADC0_BASE, 1);
  while(!ROM_ADCIntStatus(ADC0_BASE, 1, false))
  {
  }
  ROM_ADCIntClear(ADC0_BASE, 1);
  ROM_ADCSequenceDataGet(ADC0_BASE, 1, (unsigned long*) value);
  return (int32_t)(value[0]);
}

int32_t getTempAdcValue(){
  unsigned long ulADC0Value[4];

  SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0);
  SysCtlADCSpeedSet(SYSCTL_ADCSPEED_125KSPS);
  ADCSequenceDisable(ADC0_BASE, 1);
  ADCSequenceConfigure(ADC0_BASE, 1, ADC_TRIGGER_PROCESSOR, 0);
  ADCSequenceStepConfigure(ADC0_BASE, 1, 0, ADC_CTL_TS);
  ADCSequenceStepConfigure(ADC0_BASE, 1, 1, ADC_CTL_TS);
  ADCSequenceStepConfigure(ADC0_BASE, 1, 2, ADC_CTL_TS);
  ADCSequenceStepConfigure(ADC0_BASE, 1, 3, ADC_CTL_TS | ADC_CTL_IE | ADC_CTL_END);
  ADCSequenceEnable(ADC0_BASE, 1);

  ADCIntClear(ADC0_BASE, 1);
  ADCProcessorTrigger(ADC0_BASE, 1);

  while(!ADCIntStatus(ADC0_BASE, 1, false)){
  }

  ADCSequenceDataGet(ADC0_BASE, 1, ulADC0Value);
  unsigned long ulTempAvg = (ulADC0Value[0] + ulADC0Value[1] + ulADC0Value[2] + ulADC0Value[3] + 2)/4;
  int32_t ulTempValueC = 1475 - ((2475 * ulTempAvg)) / 4096;
  return ulTempValueC;
}

// Add setup code
void setup() {
  pinMode(PC_4, OUTPUT);
  digitalWrite(PC_4, HIGH);//vcc
  pinMode(PF_3, OUTPUT);
  digitalWrite(PF_3, LOW);//vss
  pinMode(PB_3, OUTPUT);
  digitalWrite(PB_3, LOW);//led

  display.begin();
  display.setContrast(50);
  display.clearDisplay();   // clears the screen and buffer

  {
    SysCtlPeripheralEnable(SYSCTL_PERIPH_ADC0);
    SysCtlADCSpeedSet(SYSCTL_ADCSPEED_125KSPS);
    ADCSequenceDisable(ADC0_BASE, 1);
  }

  ADCHardwareOversampleConfigure(ADC0_BASE, 64);

//  pinMode(PUSH1, INPUT_PULLUP);
//  pinMode(PUSH2, INPUT_PULLUP);

  Serial.begin(9600);

  //initial measurements
  {
    //    adcV1 = analogReadAveraged(v1adc);
    //    adcI1 = analogReadAveraged(i1adc);
    //    adcV2 = analogReadAveraged(v2adc);
    //    adcI2 = analogReadAveraged(i2adc);
  }

  Queue myQueue;
  myQueue.scheduleFunction(takeMeasurements, "Measure", 0, 10);
  myQueue.scheduleFunction(displayUI, "Display", 20, 200);
  myQueue.scheduleFunction(readButtons, "ReadInp", 0, 100);
  myQueue.scheduleFunction(readTemp, "ReadTem", 100, 500);

  while(1) {
    myQueue.Run(millis());
    delay(5);
  }
}

// Display mask
char temptext[8] = {
  ' ', ' ', ' ', '.', ' ', 0x7f, 'C', 0x00};

int32_t minScale;
int32_t maxScale;

void appendToBuffer(int32_t buffer[], int32_t value){
  //roll the buffer one value forward
  for (uint8_t i=1; i<RBUFFER_SIZE; i++){
    buffer[i-1] = buffer[i];
  }
  //replace last position
  buffer[RBUFFER_SIZE-1] = value;
}

void drawBuffer(uint8_t xpos, uint8_t ypos, uint8_t bufferHeight){
  minScale = 9999999; //TODO
  maxScale = -9999999;
  // retrieve min and max values
  for (uint8_t i=0; i<RBUFFER_SIZE; i++){
    minScale = min(voltages[i],minScale);
    maxScale = max(voltages[i],maxScale);
  }

  uint8_t lastX = xpos;
  uint8_t lastY = ypos;
  //draw pixels
  for (uint8_t i=0; i<RBUFFER_SIZE; i++){
    uint8_t x = xpos + i;
    uint8_t y = ypos + bufferHeight - (bufferHeight*(voltages[i]-minScale)/(maxScale-minScale));
    if (PIXEL_DOUBLING && (i%2==0)){
      //produces artifacts, rolling buffer increment should be set to 2 to avoid it
    }
    else{
      if (PIXELS_INSTEAD_OF_LINES){
        display.drawPixel(x, y, BLACK);
      }
      else{
        display.drawLine(lastX, lastY, x, y, BLACK);
      }
      lastX = x;
      lastY = y;
    }
  }
}

uint8_t ypos = 20;
uint8_t bufferHeight = 20;

// large readout on top, graph with min/max on bottom
void displayScreenGraph(){
  display.clearDisplay();

  //draw graph first so it stays under
  drawBuffer(0, ypos, 20);

  //min and max scale texts
  {
    display.setTextSize(1);
    display.setTextColor(WHITE, BLACK);
    display.setCursor(0, ypos+bufferHeight-6);
    display.print((float)minScale/10,1);

    display.setCursor(0, ypos);
    display.print((float)maxScale/10,1);
  }

  //large readout
  display.setTextSize(2);
  display.setTextColor(BLACK);
  display.setCursor(0,0);
  display.print((float)temperature/10,1);
  display.print('C');

  display.display();
}

void drawVoltageCircle(float instantValue, int centerX, int centerY, int radius){
    display.drawCircle(centerX, centerY, radius, BLACK);
    float min = PI / 4;
    float range = 3 * PI / 2;
    float radians = range * instantValue + min;
    float posX = centerX - sin(radians) * radius;
    float posY = centerY + cos(radians) * radius;
    display.drawLine(centerX, centerY, posX, posY, BLACK);
    // draw min/max helper spots
    display.drawCircle(centerX - sin(min) * (radius+1), centerY +  cos(min) * (radius+1), 1, BLACK);
    display.drawCircle(centerX - sin(min+range) * (radius+1), centerY +  cos(min+range) * (radius+1), 1, BLACK);
}

//large readout of V&A for both supplies, OVP/OVC status
void displayDualPSU(){
  float v1 = V1_ADC_COEF * adcV1Readout + 0.05;
  float i1 = - I1_ADC_COEF * (adcI1Readout - I1_ADC_OFFSET);
  float v2 = V2_ADC_COEF * adcV2Readout;
  float i2 = - I2_ADC_COEF * (adcI2Readout - I2_ADC_OFFSET);

  int radius = 9;

  display.clearDisplay();
  {// PSU1
    display.setTextSize(2);
    display.setTextColor(BLACK);
    display.setCursor(0,0);
    display.print(v1, v1>10 ? 2 : 3);
    display.println();

    display.setTextSize(1);
    //display.setCursor(0,16);
    if (i1 > 0){
    display.print("+");
    }
    display.print(i1, 3);
    display.print("A");

    //display.drawLine(0, 14, (uint8_t)(LCDWIDTH*(V1_ADC_COEF*adcV1)/V1_MAX_RANGE), 14, BLACK);
    // analog amp bar
    display.fillRoundRect(LCDWIDTH-35, 14 + 3, (uint8_t)((-I1_ADC_COEF*(adcI1 - I1_ADC_OFFSET)*35/I1_MAX_RANGE)), 6, 3, BLACK);

    drawVoltageCircle((V1_ADC_COEF*adcV1)/V1_MAX_RANGE, LCDWIDTH - radius - 1, radius, radius);

    display.setTextSize(1);
    display.setCursor(LCDWIDTH-24,0);
    display.print("v");
  }

  {// PSU2
    display.setTextSize(2);
    display.setTextColor(BLACK);
    display.setCursor(0,24);
    display.print(v2, v2>10 ? 2 : 3);
    display.println();

    display.setTextSize(1);
    if (i2 > 0){
    display.print("+");
    }
    display.print(i2, 3);
    display.print("A");

    //display.drawLine(0, 38, (uint8_t)(LCDWIDTH*(V1_ADC_COEF*adcV2)/V2_MAX_RANGE), 38, BLACK);
    // analog amp bar
    display.fillRoundRect(LCDWIDTH-35, 38 + 3, (uint8_t)((-I2_ADC_COEF*(adcI2 - I2_ADC_OFFSET)*35/I2_MAX_RANGE)), 6, 3, BLACK);

    drawVoltageCircle((V2_ADC_COEF*adcV2)/V2_MAX_RANGE, LCDWIDTH - radius - 1, 24 + radius, radius);

    display.setTextSize(1);
    display.setCursor(LCDWIDTH-24,24);
    display.print("v");
  }

  display.display();
}

int readButtons(unsigned long now){
  pinMode(PUSH1, INPUT_PULLUP);
  pinMode(PUSH2, INPUT_PULLUP);
  // read the state of the pushbutton value:
  if (digitalRead(PUSH2)==LOW) {
    while (digitalRead(PUSH2)==LOW); // debounce
    currentScreen++;
  }
  return 0;
}

int displayUI(unsigned long now){
  if (currentScreen>1) currentScreen = 0;
  if (currentScreen==0) displayDualPSU();
  if (currentScreen==1) displayScreenGraph();
  return 0;
}

int takeMeasurements(unsigned long now){
  adcV1 = analogReadAveraged(v1adc);
  adcV1Readout = ((9*adcV1Readout)+adcV1)/10;
  adcI1 = analogReadAveraged(i1adc);
  adcI1Readout = ((9*adcI1Readout)+adcI1)/10;
  adcV2 = analogReadAveraged(v2adc);
  adcV2Readout = ((9*adcV2Readout)+adcV2)/10;
  adcI2 = analogReadAveraged(i2adc);
  adcI2Readout = ((9*adcI2Readout)+adcI2)/10;

  return 0;
}

int readTemp(unsigned long now){
  temperature = getTempAdcValue();
  appendToBuffer(voltages, temperature);

  return 0;
}

// Add loop code
void loop() {

}

Conclusion

I believe in modules that do one thing and one thing well. This supply already has a lot of extra features built-in and even then it's not how I would have liked it to work.
I would probably start with a different setup, design a circuit board with known parts. It should have one display for each module with it's own separate controller and LCD.
The modules I'm thinking of:
- digitally-controlled PSU with CC and CV modes plus protection. 0-30V/0-5A
- electronic load - 0-5A
- signal generator
- multiplexer - is able to switch input/outputs from the above modules via a relay. For example charge a battery from the PSU and discharge it from the load, using the same set of terminals.

That being said, I've had some moderate fun building this as I've ran into all possible issues.

No comments:

Post a Comment