How long do solar cells live? (Part 2)

Circuit design

In this post, I want to talk about the circuit that I developed to drive solar cells at their maximum power point – the main building block of a modular lifetime tester. At this point, I should credit Sarah Sofia at MIT for her article “Build Your Own Sourcemeter“. This is what really gave me the inspiration and got me thinking that this would actually be possible with an Arduino and simple electronics.

Circuit layout of the prototype lifetime tester composed of DAC, op-amp and ADC interfaced by SPI with an Arduino UNO. The Arduino is interfaced with a PC through the serial port. Note that only one channel is shown here (one DUT).

A schematic of the lifetime tester circuit is shown above. In essence, the system is composed of:

  • a two-channel DAC (MCP4822) to give me the drive voltage across the solar cell. Because there are two channels I can run two solar cells at the same time. Typically, several subcells (6-8) are made on the same substrate so here, we can test two subcells of the same device at the same.
  • solar cell output is dumped into separate small (10-100 Ohm) series resistors which allow us to measure the current from the voltage dropped across them (applying Ohm’s law). Since resistance values and currents are small, the voltage drop will be small (we don’t want to drop much voltage in our ammeter).
  • an opamp is then needed to (on each channel) to bring the voltage to something that an ADC (ADS1286) can actually read. In fact, I’ve used an inverting op-amp with variable gain up to 1000x. To account for the fact that different solar cells under test might have different efficiencies and could therefore supply a different current, the gain is variable.

In this circuit, only the fourth quadrant (power generating region of the IV characteristic) can be accessed. Under operation, a solar cell will supply a current in the opposite direction to the applied bias. This means that the voltage across the series resistor will, in fact, be negative – one terminal is grounded, the other will be at some voltage below 0V. This signal gets fed into an inverting opamp which flips it positive again and amplifies it too. Any positive voltage at the input here will be rejected as it will be inverted to a negative output and will hit the 0V supply rail of the opamp. This means that if you try to run the solar cell in forward bias above the open-circuit voltage, giving you a forward current, you won’t get any output from the amplifier. I’ve tried to give you an illustration of this in the figure below (see lower panel).

(upper panel) LT spice model used to simulate and design the lifetime tester circuit. Note that the solar cell (DUT) has been modelled as a diode, current source and some resistors. (Lower panel) simulated input voltage from the DUT (blue line) and output (red line) voltage from the op-amp.

So did it work?…

To answer this, I connected up a solar cell and just went for a simple voltage sweep from the DAC while monitoring the current using the ADC. Here’s what the data looked like…

Using the prototype lifetime tester circuit to measure an I-V characteristic from a perovskite solar cell under illumination (black line). Power output (red line) has been calculated from DAC binary value * ADC binary value.

I was pretty happy when I saw this data. As you can see, it looks almost exactly how we expected it to from simulations and from understanding how an inverting op amp should operate. Furthermore, the fact that we can get power output curve and see a clear maximum power point (MPP) means things are looking good for doing MPP tracking. There’s some noise but I think this might have been from the fact that I was using the torch on my phone as an illumination source and it was hard to hold it exactly still. Since the measurement takes several seconds to complete, shaky hands could well be the culprit.

If you’re interested in how I coded this, then please follow the link.

How long to solar cells live?

I recently introduced DACs and ADCs. The reason that I got into this in the first place was so that I could build a cheap system for testing solar cells and ultimately measure their stability (lifetime). Perovskite solar cells are notoriously unstable and this is an area of active research right now. Clearly, a system that could monitor the efficiency of many solar cells at the same time would be really useful here.

So I got to work thinking about how we might actually do this. At the moment, this kind of measurement is done with a handful of cells kept under constant illumination with the efficiency being sampled on a timescale of minutes to hours. In between measurements, the cells will be disconnected (held at open-circuit). The illumination is fixed at an intensity of 1 Sun (100mW/cm^2). This kind of measurement really limits the amount and quality of data that we can get.

Firstly, we can’t test many solar cells at the same time (around eight) and have to wait until we’ve finished measuring all devices until we can test any others – data acquisition has to be halted and restarted by the investigator.

Secondly, we’re limited to using the same illumination intensity for all devices and that can only ever be 1 Sun (or perhaps less if you were to stick a neutral density filter over individual solar cells). Increasing illumination intensity will accelerate the test. Naively, doubling the intensity will quarter the lifetime which would remove another bottleneck in solar cell testing.

Lastly, and most importantly, leaving solar cells at open-circuit between measurements is not representative of real-world operation; solar cells need to deliver current to a load ideally at their maximum power point (MPP). At open circuit, the cell does not supply power – if we’re not going to use the power from the cell, what’s the point! One might argue that testing this way is fine for telling us about stability. However, the electric field and charge distribution inside the cell will be different here to real operating conditions, where we actually extract charge by drawing a current,  and degradation in these materials has already been linked to field assisted ion migration. Clearly, any learnings we might get using this approach would have limited practical application in developing highly stable solar cells for the real world.

Example I-V characteristics of a solar cell in the dark (black line) and under illumination (red line). Power output vs applied bias is also shown (dotted blue line) and the maximum power point (MPP) has been marked.So then the aim of the project is to build a system which can:

  1. provide high-intensity, controlled white light illumination
  2. monitor solar efficiency whilst the device is operated at MPP
  3. be modular and independent such that the number of channels can be expanded whenever the experimenter feels it’s necessary
  4. be manufactured for less than £20 per unit

System components

  • High-intensity light source – A high-intensity LED light source seemed like the natural option here. They are cheap, efficient (important if we don’t want lots of heat) and capable of delivering lots of light power which is exactly what we want. On the downside, they may not match the solar spectrum all that well. Solar simulators are classified according to how well they can reproduce the Sun’s illumination.
  • Basic source measurement unit (SMU) module – To characterise solar cells, a SMU is the instrument of choice.  It allows us to precisely control the voltage and read off current in either direction so that we can see all four quadrants of the IV characteristic. Commercially available Keithley SMUs tend to cost in the £1000’s so will obviously be out of our price range for this project. Still, we’re going to need something that can fulfil the role of monitoring power output and maintaining MPP during the lifetime test. I found a really useful article here describing how to build your own SMU from an Arduino and a DAC which I adapted to suit my needs.
  • Data acquisition and transfer to a central unit – As the solar cell is driven, the voltage, current and power output data as a function of time need to be transferred to a central unit that is interfaced with a computer (or SD card interface perhaps). This data will then be accessible to a user for further analysis offline.

In the coming series of posts, I’m going to detail what I did here including circuit design, testing and code. Watch this space…

Portable robotic spray-coater project

For the last couple of years I’ve been working on spray-coated solar cells at the University of Sheffield in the EPMM group. This group is doing lots of interesting work on thin-film next-generation low-cost solar cells and spray-coating is a great way to go about making them; it’s fast, versatile and scalable. Instead of using blocks of silicon, active (functional) materials are  dissolved in solvents to make inks. Then inks can be  processed using conventional printing techniques, or even sprayed, to leave thin-films that can harvest light or transport charge which, through careful optimisation, gives us a solar cell.

I’ve been used to using ultrasonic spray-coaters in my work but these instruments are really bulky and not at all portable. What I wanted was a system that we could use in Sheffield to make solar cells in the lab but would also be portable enough for us to take to an Xray light source. This would enable us to start to understand the way that semiconducting films form in real-time at the nanoscale.

So the brief was to make a portable spray-coater suitable for solar cell fabrication that was programmable and would give reasonably repeatable processes. To address this, my basic idea was to strip out a linear drive system from an inkjet printer and mount a simple artists airbrush on the carriage which I would control with a solenoid valve. With an Arduino microcontroller and some embedded software, I was able to control the drive system and solenoid valve to spray lines of water as you can see in the video. At this point, I finished my contract so I wasn’t able to take this any work any further. But now that the kit is built, it would be possible to do this project. Watch this space. Below I’m going to lay out the schematics and details of how I actually did it!

As a Christmas present to myself, I bought the Ardiuno starter kit. It really kick started things for me and took me from absolute ignorance to a “light-bulb moment”. Originally, I got the idea to use the H-Bridge project (Zeotrope project No. 10) form the kit. In the kit you get an L293 H-Bridge, a circuit and code to drive it among lots of other stuff. In a nutshell, an H-Bridge is an IC composed of several MOSFETs which you can use to control a small DC motor (speed/direction/on-off). I quickly realised that this wasn’t going to cut it however – the load requirement of the motor I was using was too much for the poor little chap. The peak output is only 1.2A and after some digging, I discovered that the motor in the drive unit would draw at least 1A at no load and up to 40A if stalled! You’d need some serious power electronics for this so made the assumption that I need to uprate things a little and I went for the Arduino motor shield which is rated at 2A (per channel). Turned out that this was enough to drive the motor and with pulse width modulation and some logic signals, I could control speed and direction of the motor.

Circuit based on the “Zeotrope” project that I borrowed from the Arduino starter kit. This is what I used before I realised that the H-Bridge provided wasn’t able to deliver enough current for the motor and just overheated. There are a couple of momentary switches for on/off and direction functions and a pot for speed control.

To read the carriage position, you need to know about quadrature encoders. I found a nice tutorial on this here. Amazingly, it’s possible to get to an accuracy of 0.07mm! But if you want to print at high resolution then I guess that’s not all that surprising. For this application, we don’t need anywhere near this accuracy – cm accuracy is probably about good enough but it’s nice to have it all the same. The position is read by a light gate and encoder strip which is a piece of acetate with lots of lines printed really close together. As the carriage moves along, we see a square wave from the light gate as the lines break the beam and if we count these, then we can follow the position of the printer head. Neat! The code is relatively straightforward. So, all that was left was to integrate these two functions and sort out some of the details such as an interface, power supplies, solenoid valve and some hardware to mount everything.

Linear drive system

HP inkjet printer in a state of disassembly. You can see the paper feed and drive system are starting to come away from each other. It took me an evening to get to this point and was lots of fun.
Inside the printer carriage. I’ve removed the carriage from the rail and opened it up. There is a drive belt in the front right of the image and the encoder is hidden from view underneath the ribbon cable at the back. I was surprised how much stuff was in here and a bit puzzled about how to access the signal from the encoder so this part took some time.
The control board from inside the printer carriage. I decided to remove the board and cables to get a good look at things. This is the back. I soldered this patch lead to the back after figuring out what did what and ran the leads out to the Arduino. All the other stuff was now dead weight but I couldn’t remove it because it keeps the encoder reader in exactly the right place to read the strip.

Final schematics and boards

Motor shield and additional homemade boards plugged in. Right-hand side: 24V (PSU) to 7.2V (motor) DC-DC converter and MOSFET for driving the spray solenoid valve. Left-hand side: LED indicators with current limiting resistors.
Circuit diagram. Note: the motor shield is not shown.

Here (see video below), you can see me testing out the printer carriage with the boards that I made. Note that the movement is a bit jerky because I removed a retaining strip from above the carriage so I could have electrical access – this makes the carriage droop downwards and seems to add a bit of friction.

The code

//yellow is Q line?
//blue is I line?

// Interrupt information
// 0 on pin 2
// 1 on pin 3

#define encoderI 2
#define encoderQ 3 // Only use one interrupt in this example

volatile int count;
void setup()
{
  Serial.begin(9600);
  count=0;
  pinMode(encoderI, INPUT);
  pinMode(encoderQ, INPUT); attachInterrupt(0, handleEncoder, CHANGE);

}

void loop()
{
  Serial.println(count);
  delay(10);
}

void handleEncoder()
{
  if(digitalRead(encoderI) == digitalRead(encoderQ))
  {
    count++;
  }
  else
  { 
    count--;
  }
}

Above is the code that I used to check that I could read positions from the quadrature encoder based on this youtube resource. It worked really nicely and so I moved on to controlling the motor and solenoid valve using a serial interface.

//130217 code for controlling printer head spray coater
//commands issued as ascii commands via serial bus (USB)
//version uses the motor shield

/*
COMMANDS
AXXXX start position
BXXXX finish position
VXXX speed
DXXXX delay in ms
*/

//yellow is Q line - pin3
//blue is I line - pin2

// Interrupt information
// 0 on pin 2 blue
// 1 on pin 3 yellow

#define encoderI 2
#define encoderQ 3 // Only use one interrupt in this example

volatile int count; //current position index
int startPos = 0; //start of dep
int finPos = 0; //end of dep
int xPos = 0;
int myDelay = 0; //delay after starting spray but before move
String inputString = ""; //holds serial commands

const int stopDistance = 2; //factor to determine stopping distance

const int controlPin = 13; // channel B direction
const int enablePin = 11;   // channel B PWM input
const int solenoidPin = 4;  //solenoid control pin for starting spray
const int solenoidOverride = 7; // manually turn solenoid on
const int solenoidLED = 5;
const int motorLED = 6;

int motorSpeed = 220;
int homeSpeed = 220;
int approachSpeed = 200; //slower speed for approaching target position so you don't overshoot

void setup()
{
  Serial.begin(9600);
  count=0;

  pinMode(encoderI, INPUT);
  pinMode(encoderQ, INPUT); attachInterrupt(0, handleEncoder, CHANGE);
  
  // intialize the inputs and outputs
  pinMode(controlPin, OUTPUT);
  pinMode(enablePin, OUTPUT);
  pinMode(solenoidPin, OUTPUT);
  pinMode(solenoidOverride, INPUT);
  pinMode(solenoidLED, OUTPUT);
  pinMode(motorLED, OUTPUT);
  // pull outputs LOW to start
  digitalWrite(enablePin, LOW); 
  digitalWrite(solenoidPin, LOW);
  digitalWrite(controlPin, LOW);
  digitalWrite(solenoidLED, LOW);
  digitalWrite(motorLED, LOW);
  // reserve 200 bytes for the inputString:
  inputString.reserve(200);
}

void loop() {
   int readUserSpeed;
   
   //decide which function to do with the switch case
   switch (inputString.charAt(0)){
    case 'X':
      xPos = inputString.substring(1).toInt();
      if (xPos < 0 || xPos > 4100){
        Serial.println("Outside allowed range 0-4100");
        xPos = 0;
      }
      Serial.print("Moving to ");
      Serial.println(xPos);
      moveStage(xPos);
    break;
    case 'A':
      startPos = inputString.substring(1).toInt();
      if (startPos < 0 || startPos > 4100){
        Serial.println("Outside allowed range 0-4100");
        startPos = 0;
      }
      Serial.print("Start deposition at ");
      Serial.println(startPos);
    break;
    case 'B':
      finPos = inputString.substring(1).toInt();
      if (finPos < 0 || finPos > 4100){
        Serial.println("Outside allowed range 0-4100");
        finPos = 0;
      }
      else {
      Serial.print("Finish deposition at ");
      Serial.println(finPos);
      }
    break;
    case 'D':
      myDelay = inputString.substring(1).toInt();
      if (myDelay < 0 || myDelay > 9999){
        Serial.println("Outside allowed range 0-9999");
        myDelay = 0;
      }
      else {
      Serial.print("delay (ms) ");
      Serial.println(myDelay);
      }
    break;
    case 'V':
      readUserSpeed = inputString.substring(1).toInt(); //need to make sure this is between 0-255
      if (readUserSpeed < 0 || readUserSpeed > 255){
        Serial.println("Outside allowed range 0-255");
      }
      else {
        motorSpeed = readUserSpeed;
      }
      Serial.print("Motor speed set to ");
      Serial.println(motorSpeed);
    break;
    case 'H':
      homeStage();
    break;
    case 'Q':  //query
      Serial.println("Current settings:");
      Serial.print("Motor speed set to ");
      Serial.println(motorSpeed);
      Serial.print("Start deposition at ");
      Serial.println(startPos);
      Serial.print("Finish deposition at ");
      Serial.println(finPos);
      Serial.print("delay (ms) ");
      Serial.println(myDelay);
      Serial.print("current position ");
      Serial.println(count);
    break;
    case 'R': //run recipe
      Serial.println("Run recipe...");
      moveStage(startPos);
      digitalWrite(solenoidPin, HIGH);
      digitalWrite(solenoidLED, HIGH);
      delay(myDelay);
      moveStage(finPos);
      digitalWrite(solenoidPin, LOW);
      digitalWrite(solenoidLED, LOW);
      Serial.println("recipe done.");   
    break;
    default:
      digitalWrite(enablePin, LOW);
   }

//override the solenoid when you push the button down
if (digitalRead(solenoidOverride)){
  digitalWrite(solenoidPin, HIGH);
  digitalWrite(solenoidLED, HIGH);
}
else {
  digitalWrite(solenoidPin, LOW);
  digitalWrite(solenoidLED, LOW);
}

inputString = "";
delay(10);     
}
    
//update position counter
void handleEncoder()
{
if(digitalRead(encoderI) == digitalRead(encoderQ))
{ count--;
}
else
{ count++;
}

}
//int dirState = 1; // 0/1 for R-to-L (count++)/L-to-R (count--) 
void motorDirection(int dirState) {
    // change the direction the motor spins by talking
  // to the control pins on the H-Bridge
  if (dirState == 1) {
    digitalWrite(controlPin, LOW);
  } else {
    digitalWrite(controlPin, HIGH);
  }
}

//read serial data if there is any
void serialEvent() {
  if (Serial.available() > 1) {
      inputString = Serial.readString();
      Serial.println(inputString);
  }
}

//better way to write things is to separate into functions for different things and then call within the serial.event
void homeStage(){ //initialise homing - //go left until you stop moving
      Serial.println("Homing...");

      int homePos = count + 1; //force into loop
      motorDirection(0);
      analogWrite(enablePin, homeSpeed);
      digitalWrite(motorLED, HIGH);
      
      while (homePos > count) {
          homePos = count;
          delay(50);
                Serial.println(count);
      }
      
      count = 0;
      analogWrite(enablePin, 0);
      digitalWrite(motorLED, LOW);
      Serial.println("Done");
  }

void moveStage(int targetPos) { //move stage function
    
    Serial.print("Moving stage to ");
    Serial.println(targetPos);
    
    while (abs(targetPos-count)>5){
    Serial.println(count);
      // work out direction - 0/1 for R-to-L (count--)/L-to-R (count++) 
      if (targetPos > count) {
        motorDirection(1);
      }
      else if (targetPos < count) {
        motorDirection(0);
      }
      
      //check how far we are away from the target and set motor speed     
      if (abs(targetPos-count)<((motorSpeed-180)*stopDistance)) {
        // PWM the enable pin to vary the speed
        //going faster means you need to slow down earlier
        //use motorspeed or similar to manage stopping distance
        analogWrite(enablePin, approachSpeed);
        digitalWrite(motorLED, HIGH);
      }
      else {
        analogWrite(enablePin, motorSpeed); //otherwise go full speed
        digitalWrite(motorLED, HIGH);
      }
      delay(1); //think the delay is important to stop it checking position too often
      
    }
    analogWrite(enablePin, 0);
    digitalWrite(motorLED, LOW);
    Serial.println("Done"); //this doesn't print out. Why?

}