A State Machine – Unit Testing

Last time I talked about how I implemented a state-machine to control the lifetester that I’ve been developing. In the process, I relied heavily on unit testing the code as I wrote it. In fact, by unit testing the code while refactoring, all the development was done on my desktop machine! This is a big departure from how I used to do things a year ago where all the development that I did was using the Arduino IDE and code was compiled and run on the target. I only needed to compile for the Arduino and plug in a reference solar cell at the very end to check that everything worked as I expected and I’m pleased to say that it did. My eyes were opened to this in my first job as an embedded software engineer at CMR. I found this was one of the most striking differences between the professional and home project software development worlds. In essence, unit testing is software designed to exercise all of the behaviour of the code (as independent units) we’re intending to write. We’re trying to check that it works as we expect, whether we give the functions good or bad inputs – there are positive and negative tests. It was put to me like this once: “Unit-test the code like a burglar rather than a postman”.

How to write unit tests

I think it’s worth saying here that there are good and bad ways of unit testing and as with anything. I really like this guide. In short, unit-tests should be F.I.R.S.T:

  • Fast – probably more important on very large projects than here but who wants to wait ages for their tests to run?
  • Independent/Isolated – tests should follow the arrange, act assert format.
  • Repeatable – the order that test run shouldn’t matter and the results should be the same every time they’re run. They should be responsible for setup and teardown all of their data.
  • Self-validating – we don’t need to inspect anything to see if a test has passed or failed. The results should be reported automatically.
  • Thorough and timely – cover every use scenario and be done drive the development of the source code not be written later.

My attempt

Clearly, I can’t go through all the unit tests for this module as this post would be far too long but I can show you a couple of examples to give you an idea how this might work. Here goes…

TEST(IVTestGroup, SaturatedCurrentDetectedIncrementsErrorReadingsCounter)
{
    mockLifeTester->data.delayDone = true;
    mockLifeTester->data.iSampleSum = MAX_CURRENT;
    mockLifeTester->data.nSamples = 1U;
    mockLifeTester->data.nErrorReads = 0U;
    mockLifeTester->state = &StateMeasureThisDataPoint;
    const uint32_t tInit = 34524U;
    mockTime = tInit + SETTLE_TIME + SAMPLING_TIME;
    ActivateThisMeasurement(mockLifeTester);
    MocksForTrackingModeStep();
    MocksForMeasureDataNoAdcRead();
    StateMachine_UpdateStep(mockLifeTester);
    POINTERS_EQUAL(&StateTrackingMode, mockLifeTester->state);
    CHECK_EQUAL(1U, mockLifeTester->data.nErrorReads);
    CHECK_EQUAL(currentLimit, mockLifeTester->error);
    mock().checkExpectations();
}

Above is a test the checks that if the adc readings are saturated (the current from the device goes outside the available range), the reading is counted as a bad reading and added to a counter – the lifetester should accepts a few bad readings before transitioning to the error state. In the test module, I’ve setup a mockLifeTester variable (instance) that I reset before every test. So the first thing to do in the test is set the lifetester to the correct mode (MeasureThisDataPoint), reset the error readings counter and most importantly saturate the current reading. mockTime is my way of returning a value from millis() in the source code. You can see that I’ve incremented the timer so that the sampling window and tracking delay have expired before calling the state-machine update function. Now I do the asserts and check that we’ve transitioned back to TrackingMode (parent state), as the sampling time is over and that the error has been counted and recorded in the mockLifeTester data. Of course, too many bad readings should lead to a transition to the StateError as follows…

TEST(IVTestGroup, TrackingModeTooManyBadReadingsTransitionToErrorState)
{
    // Setup for tracking mode.
    mockLifeTester->data.nErrorReads = MAX_ERROR_READS + 1U;
    mockLifeTester->state = &StateTrackingMode;
    MocksForTrackingModeStep();
    MocksForErrorEntry(mockLifeTester);
    StateMachine_UpdateStep(mockLifeTester);
    POINTERS_EQUAL(&StateError, mockLifeTester->state);
    mock().checkExpectations();
}

All I need to do here is setup the the number of error readings above the allowed limit before calling the update function. This should lead to a transition to the error mode which will have happened for the test to pass in the POINTERS_EQUAL(...) statement.

Mocking

The question on my mind before I began unit-testing embedded code was “How do we execute code written for an embedded platform on a PC? Won’t it try to call hardware specific functions that don’t exist?”. This is accomplished by mocking – any calls to low level i/o are replaced with our own mock functions. I’ve given an example here that I needed in the last test…

static void MocksForErrorLedSetup(void)
{
    mock().expectOneCall("Flasher::t")
        .withParameter("onNew", ERROR_LED_ON_TIME)
        .withParameter("offNew", ERROR_LED_OFF_TIME);
    mock().expectOneCall("Flasher::keepFlashing");
}

static void MocksForSetDacToVoltage(LifeTester_t const *const lifeTester,
                                    uint8_t v)
{
    mock().expectOneCall("DacSetOutput")
        .withParameter("output", v)
        .withParameter("channel", lifeTester->io.dac);
}

static void MocksForErrorEntry(LifeTester_t const *const lifeTester)
{
    MocksForErrorLedSetup();
    MocksForSetDacToVoltage(lifeTester, 0U);
}

When the lifetester transitions into the error state, we expect it to set the dac and setup the flash rate of an led to indicate that the device is in its error state. So we expect our mock functions to be called. You’ll see in the tests that there’s this statement mock().checkExpectations() which is responsible for checking that the correct mocks are actually called number of times we expect. If we don’t say expectOneCall("DacSetOutput"), and a call is made to this function by the source then the test will fail. Alternatively, if we do make an expect and the function isn’t called, the test will fail too; mocking and expects are a really important tool for checking the behaviour of our code too. Asserting on the data returned is only half the picture.

 Thoughts

In this post, I’ve discussed in brief how unit testing can be used to refactor and write code that’s more robust. I hope you like it. Personally, I write unit tests for all the code that I write now even though it means you have to write twice as much code. I like the way that it helps me to think through what I’m doing and encourages me to write cleaner code where the lower layers are abstracted so they can be mocked effectively. I believe that it could be better understood and used by the ‘hacker’ community to good effect…but then would it really be hacking?

How long do solar cells live? (maximum power point tracking)

In other posts, I’ve talked about developing the lifetester board and output from the prototypes that I’ve built. So far however, I haven’t given any detail on how maximum point tracking actually works and in this post, I want to unravel things a bit. For this first attempt, I’ve gone for a really simple hill-climbing algorithm which looks like this:

In summary, It does the following steps to update the drive voltage to maintain the MPP:

  1. Scan the drive voltage and look for the maximum power point to be used as an initial guess (not shown).
  2. Set the drive voltage (V) for this point, measure the current.
  3. Set the drive voltage (V + dV) for the next point, measure the current.
  4. If Power(next) > Power(this), set V -= dV else set V += dV.
  5. Repeat step 2.

In software, the update (step) function looks like this:

void IV_MpptUpdate(LifeTester_t *const lifeTester)
{
    uint32_t tElapsed = millis() - lifeTester->timer;
  
    if ((lifeTester->error != currentThreshold)
        && (lifeTester->nErrorReads < MAX_ERROR_READS))
    {
        if ((tElapsed >= TRACK_DELAY_TIME)
            && tElapsed < (TRACK_DELAY_TIME + SETTLE_TIME))
        {
            //STAGE 1: SET INITIAL STATE OF DAC V0
            DacSetOutput(lifeTester->IVData.v, lifeTester->channel.dac);
        }
        else if ((tElapsed >= (TRACK_DELAY_TIME + SETTLE_TIME))
                 && (tElapsed < (TRACK_DELAY_TIME + SETTLE_TIME + SAMPLING_TIME)))
        {
            //STAGE 2: KEEP READING THE CURRENT AND SUMMING IT AFTER THE SETTLE TIME
            lifeTester->IVData.iCurrent += AdcReadData(lifeTester->channel.adc);
            lifeTester->nReadsCurrent++;
        }    
        else if ((tElapsed >= (TRACK_DELAY_TIME + SETTLE_TIME + SAMPLING_TIME))
                 && (tElapsed < (TRACK_DELAY_TIME + 2 * SETTLE_TIME + SAMPLING_TIME)))
        {
            //STAGE 3: STOP SAMPLING. SET DAC TO V1
            DacSetOutput((lifeTester->IVData.v + DV_MPPT), lifeTester->channel.dac);
        }
        else if ((tElapsed >= (TRACK_DELAY_TIME + 2 * SETTLE_TIME + SAMPLING_TIME))
                 && (tElapsed < (TRACK_DELAY_TIME + 2 * SETTLE_TIME + 2 * SAMPLING_TIME)))
        {
            //STAGE 4: KEEP READING THE CURRENT AND SUMMING IT AFTER ANOTHER SETTLE TIME
            lifeTester->IVData.iNext += AdcReadData(lifeTester->channel.adc);
            lifeTester->nReadsNext++;
        }
        //STAGE 5: MEASUREMENTS DONE. DO CALCULATIONS
        else if (tElapsed >= (TRACK_DELAY_TIME + 2 * SETTLE_TIME + 2 * SAMPLING_TIME))
        {
            // Readings are summed together and then averaged.
            lifeTester->IVData.iCurrent /= lifeTester->nReadsCurrent;
            lifeTester->IVData.pCurrent =
                lifeTester->IVData.v * lifeTester->IVData.iCurrent;
            lifeTester->nReadsCurrent = 0;

            lifeTester->IVData.iNext /= lifeTester->nReadsNext;
            lifeTester->IVData.pNext =
                (lifeTester->IVData.v + DV_MPPT) * lifeTester->IVData.iNext;
            lifeTester->nReadsNext = 0;

            // if power is lower here, we must be going downhill then move back one point for next loop
            if (lifeTester->IVData.pNext > lifeTester->IVData.pCurrent)
            {
                lifeTester->IVData.v += DV_MPPT;
                lifeTester->Led.stopAfter(2); //two flashes
            }
            else
            {
                lifeTester->IVData.v -= DV_MPPT;
                lifeTester->Led.stopAfter(1); //one flash
            }
            // finished measurement now so do error detection
            if (lifeTester->IVData.iCurrent < MIN_CURRENT)
            {
                lifeTester->error = lowCurrent;
                lifeTester->nErrorReads++;
            }
            else if (lifeTester->IVData.iCurrent >= MAX_CURRENT)
            {
                lifeTester->error = currentLimit;  //reached current limit
                lifeTester->nErrorReads++;
            }
            else //no error here so reset error counter and err_code to 0
            {
                lifeTester->error = ok;
                lifeTester->nErrorReads = 0;
            }
            PrintLifeTesterData(lifeTester);

            lifeTester->IVData.iTransmit =
                0.5 * (lifeTester->IVData.iCurrent + lifeTester->IVData.iNext);
            lifeTester->timer = millis(); //reset timer
            lifeTester->IVData.iCurrent = 0;
            lifeTester->IVData.iNext = 0;
        }    
    }
    else //error condition - trigger LED
    {
        lifeTester->Led.t(500,500);
        lifeTester->Led.keepFlashing();
    }
}

This function operates on a custom lifetester type that contains all the relevant information regarding the state of the device under test. We pass a pointer to this data which the update function works on. It’s a psudo-object oriented approach. C is obviously not an object oriented language but by using a struct like an instance, this function is a bit like a method. This way, we can have another lifetester instance to represent another device under test or many more if we choose and they should not interact.

As illustrated in this solution, it’s important not to block the microcontroller with calls to delay(). If you call this, the device won’t be able to update another channel say or the state of leds. I wrote this code almost a year ago and although it works, it’s not clean:

  • The function is too long – it’s doing more than one thing.
  • There are unnecessary comments. If the code were written well, it would be self-documenting.
  • There is duplication: this point and next point share almost identical code.
  • Spot the magic numbers.

I’ve now refactored this code by means of a state-machine and will present it in a coming article.

How long do solar cells live? (part 3)

Finally, after much tinkering, I’ve got a system that’s worth committing to a PCB. Here is a shot of the prototype system being tested out…

A prototype breadboard lifetester being tested. Two solar cells are being held at MPP at the same time under the work-lamp. The arduino boards are used for PC interfacing and programming.

Above is a picture that I took as I was working on the system. At this point, two solar cells (under the work lamp) are illuminated and being driven at maximum power point (MPP) at the same time. As described previously, I used a current sensing circuit based on an inverting amplifier which is assembled on the long breadboard in the middle along with the DACs and ADCs needed to drive the circuit and collect data. On the neighbouring breadboard is a programmed ATMega328 chip which drives this process and is interfaced by I2C as a slave to another master ATMega328 on an Arduino UNO board. I needed another Arduino UNO board for programming the ATMega and for USB-Serial communication debugging when needed. There’s a neat article on this on the Arduino site here. Have a look at this schematic below for more detail of what I did exactly…

Schematic showing the layout of microcontroller and Arduino boards used in the picture above. Note that the analog circuit and SPI devices aren’t shown.

Unfortunately, the analog circuit that I was using was not quite doing the job. I noticed that although the output voltage from the DAC was as expected from the binary code that I was feeding into it, at the other end of the buffer amplifier (at the DUT terminal) it wasn’t. In particular, at Vin = 0V (short-circuit), the applied bias wasn’t 0V. It turns out that the buffer amplifier needs to work as a current sink in this case – current actually flows from ground to the buffer. To overcome this, in addition to +5V and 0V,  I also needed to supply -5V to the op-amp. To make sure that the output from the amplifier to the ADC, Vout, never went below 0V I used a precision rectifier circuit – it acts like an ideal diode; there’s no voltage drop at the output which is commonly associated with a regular diode. The simplified schematic is below and a full Fritzing file here.

Analog current sensing circuit used to drive the solar cells under test (DUT). The circuit is based on a precision rectifier/inverting amplifier. The range can be altered by changing Rsense.

Here’s what it does again in brief:

  1. Under illumination, current flows from ground to the buffer amplifier.
  2. Current flowing from ground to the buffer amplifier leads to a small (0 > Vx > -10mV) negative voltage across the sense resistor.
  3. This voltage is fed into an inverting op-amp. It is inverted and amplified 350 times. A precision rectifier arrangement ensures that the output can never go below 0V. Gain and offset can be tuned by means of trimmer resistors.
  4. The output is connected to an ADC for data logging and MPP tracking.

Below is some of the data that came out of this system…

Data measured from the prototype breadboard lifetime tester: live MPPT vs time (top panel) and the solar cell IV characteristic measured at the end of the test (bottom panel). Note that the MPP (DACx = 760) agrees well with the DAC setting during tracking.

The live MPPT data shows some fluctuation in voltage. Because of the hill climbing, perturb and observe algorithm used, the voltage is constantly being probed. You can also see a sharp step in the MPP data where I adjusted light intensity which is indicated by the increase in ADCx (current). Shortly afterwards (measurements are taken roughly every second), this is followed by DACx (applied voltage) as the MPPT system catches up which is expected. As a double check, I reset the lifetime tester to run another IV characteristic without changing the light intensity. This registered an MPP at DACx = 760 (0.38V) which was consistent with the MPP tracking data.

Having convinced myself that this system was working nicely, I decided it was time to design a PCB. More on that to come.

A solar simulator on a budget

Light source

To get the high light intensities that I needed for this project, I hunted around for a high-performance high colour temperature white LED and came up with this one. It’s a Cree XLamp CXA2520 high lumen output and efficacy LED array. I chose the 5000K version as I wanted something that would be closer to sunlight. The device delivers 2500Lm white light at 36V and draws 0.5A. I liked the fact that it was a chip-on-board assembly that was ready to mount. I tried a smaller device but cracked it when I tried to mount it on a heatsink.

Heat considerations

We really need a heatsink here because, even though LEDs are efficient, there is still quite a lot of heat to get rid of – 20W if we assume that all electrical power is converted to heat (obviously this is the worst case scenario given that a significant amount of power should be converted into light and radiated away*). Keeping the temperature down increases the efficiency of the system and the lifetime of the LED. More importantly, we don’t want to alter the environment around our solar cell too much as this would bring in an uncontrolled variable.

I found a CPU fan/heatsink laying around and looked into bonding it using adhesive thermal tape. Assuming the thermal resistance of the fan/heatsink is 0.4K/W, and that the ambient temperature is 20C, then the heatsink will run at 28C – hopefully, the LED will be in equilibrium with this so will also be at the same temperature. I checked the specs of the heat transfer adhesive and it seems its performance is predicted to be really good. To be able to transfer 20W heat power, it would need a temperature difference of only 0.001mK across it – so the LED would be at pretty much the same temperature as the heatsink surface we can assume.

Cree XLamp CXA2520 mounted on a CPU heatsink/fan under operation at very low current. Note that the masking tape shown here was removed for final testing.

Power output

This is the most important part: the power output calculation. We need to know how much light the LED actually is going to deliver to our solar cell – in the lifetime tester application, I envisage that each solar cell under test will be assigned its own LED and this way the system would be truly modular.

On to the calculations then…What we want to know is the light intensity (irradiance) on the solar cell front surface which is simply the light power per unit area. For instance, 1 Sun illumination has an intensity equal to 1kW/m2 which is itself a unit of irradiance. Here’s how we work this out:

  • The thing is we want to know how much “real” power the LED emits in Watts. Basically, our eyes are setup to be sensitive to some wavelengths over others (the peak of the eye response happens to be tuned to the sun’s peak emission per nm which is green light at around 500nm – let’s not get drawn into a discussion about evolution here). Measuring the light output in Lumens tells us how bright the LED will be to our eyes but doesn’t tell us how much power there actually is. We need to convert units and to do this, we need to know what colour the light is. If you remember, I said that our eyes have a peak sensitivity to green light. So green light has the most number of Lumens per Watt, 683 Lm/W. Other wavelengths have less. This Lm/W number is referred to as luminous efficacy of radiation – it relates luminous to radiative flux and tells us...for a given amount of light energy, how much does this stimulate our eyes. Weird huh. Don’t get this confused with luminous efficacy of the source which is a measure of the overall efficiency of the LED in converting Watts of electrical input into Lumens of emitted light (126 Lm/W in this case). In fact, increasing luminous efficacy is one way to increase the LEDs apparent efficiency; if we made it green, it would be about twice as efficient.
  • But we don’t have a green monochromatic light source, we have a white light source? So we need to average the contribution from all the different wavelengths that make up the emitted spectrum from the LED. This gets a bit complicated. Fortunately, we can make some assumptions. Let’s assume that the spectrum of the LED approximates a blackbody that has been truncated to the visible region (normally a blackbody emitter would radiate light in the NIR and UV that we can’t see so the luminous efficacy would be much lower overall). So the luminous efficacy of radiation will be 350 Lm/W. From this, we know the total radiant power output from the LED will be 2500 / 350 = 7.1 W. We’re getting there.
  • The total radiant power is helpful but we need to know about intensity, or the number of Watts emitted over a given area. One way to go would be to assume that it’s distributed evenly over space but a better way is to assume that light emission follows Lambert’s cosine law; lambertian sources have the same brightness no matter at what angle you look at them even though their emission is not uniform. Let’s not get too drawn into the specifics here other than to say that the light intensity follows a cosine law with angle and LEDs are often approximated to lambertian emitters. So why break tradition? OK then we can now say that the peak intensity in the forward direction will be 7.1 W / π = 2.3 W / sr where sr stands for steradian (a unit of angle in 3D space. Imagine the surface of a sphere rather than the arc of a circle).
  • To get the power on the front surface of our solar cell then, we just need to know how many steradians it covers and multiply.  For a 2mm x 2mm (0.04 cm2) solar cell positioned 2 cm away from the LED (face on), I expect it to cover approximately 0.031 sr (using the formula for a cone with spherical cap) which would give us 71.3 mW incident flux and an intensity of 1781 mW / cm2 or 18 Suns! At a more reasonable distance of 5 cm, we would still have 3 Suns which would be plenty.

I’ve included the details of all these calculations in this sheet.

Mounting

When I mounted the LED, I was concerned about applying enough pressure to ensure a strong bond and good thermal contact. Here, they recommend pressures in excess of 100psi! I managed only 8psi. Basically, I was concerned about breaking the LED board. I had to rest a power supply on top of a toothpick box – it seemed to be just the right size to clear the LED optical surface which shouldn’t be touched. Everything was a bit unstable as you can see…

Mounting the LED onto a CPU heatsink/fan with thermal adhesive film. Pressure applied using a small open box with a power supply on top giving 8psi.
Testing out the high power LED at 34V. Note that my power supply could only deliver 31V so I had to wire a couple of C (1.5V) batteries in series with it to get up to a more suitable voltage. You can see the meter is reading a current of 0.223A rather than the recommended 0.5A.

Driving circuit

I wired up a constant current LED driver to drive the LED with a potentiometer to control brightness (see schematics below). You can see from the chart that the output scales linearly with the voltage input to the dimmer pin – at 0V, you get the maximum output and at 4.2-4.3V the output has dropped right down to 0%.

The layout of the LED driver circuit based on the RECOM constant current LED driver unit. Output power can be controlled using the potentiometer which varies voltage supplied to the dimmer input. The output current as a function of the dimmer control voltage is also shown.

This appeared to work well when I tested it out. It got fairly bright as I adjusted the dimmer voltage which you can see from the image above however, I don’t have a way of actually measuring this at present. What I need is a calibrated meter. Unfortunately, this is outside the price range of the shed right now but I intend to do this when I visit the labs in Sheffield again.

Mismatch factor

An important figure of merit when it comes to benchmarking solar simulators is the concept of mismatch factor. It’s basically a score that your light source gets on how well it represents the solar spectrum. To work it out, we need to sum up the power in wavelength intervals over the visible and near infra-red portions of the electromagnetic spectrum for the sun (reference) and simulator (LED). Have a look at this figure below…

Calculating spectral mismatch factor: LED vs solar spectrum. Upper panel: relative spectral irradiance (area normalised) for the sun (red line) and our LED (black line). Lower panel: a table of integrated intensity over specified wavelength interval with mismatch factor (rightmost column).

Hopefully, you can see straight away that there’s a big difference in the shape of the two spectra. They have been area normalised – remember that the area under the spectra is the total power from the two sources. If we divide by area under the entire spectrum, then we’re effectively setting them to the same power for comparison which is what you would do when testing a solar cell. To get the mismatch, we then sum up the areas under the spectra between the intervals shown and compare (see table). You can see that the LED has a lot of its output in the visible range (400-700 nm) and none in the NIR compared to the sun.

To qualify as a class A solar simulator, the ratio (last column in the table) of all ranges needs to stay within 0.75 – 1.25 – we’re way off! Unfortunately, for this LED, the ratio even goes outside the allowed limits for class C (0.4 – 2.0). We need some NIR component to the spectrum to fix this which is possible. For the purposes of lifetime testing on a  budget however, then I think we need to accept these limitations. It’s good to know what they are though.

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…

My guide on using an analogue-to-digital converter

In this example, I’ve wired up an Analogue-to-digital converter (ADC) and showed you some code that will get this device talking to an Arduino using the SPI connection. There’s a potentiometer here that you can use to change the input voltage and it’s read once per second. Note that ADCs are capable of reading at much higher rates than this – according to the data sheet, the ADS1286 can sample at up to 20kHz but higher rates are possible.

ADC (ADS1286) circuit diagram. Device connected to Arduino Uno board with communication provided via SPI.

SPI stands for Serial Peripheral Interface and is a really impressive invention that allows ICs to talk to one another over short distances (on a PCB) using three wires: (1) clock, (2) master-out-slave-in (MOSI)/master-in-slave-out (MISO) and (3) chip select. In our case, since we’re dealing with receiving data from the slave (ADC), then we only use the MISO line. Wikipedia and Arduino have really good entries explaining this. However, I think that the timing diagram on Wikipedia is really helpful. Basically, when we want to talk to a chip over SPI, we send the chip select pin low, then send/read data over the MOSI/MISO data line. The whole thing is synchronised by clock pulses on the clock line. Each time the clock switches state, we end up reading another bit. The exact protocol of whether we (a) read bits on a low-high or high-low clock pulse and (b) whether bits are read on the leading or trailing edge of the clock is set in the SPI settings in the code (see below). Here is the exact operating sequence from the datasheet:

ADS1286 operating sequence straight from the datasheet. It’s not clear from this diagram but the clock polarity is 0 ie. it’s normally low and data bits are read on the trailing edge of the clock pulse. We read two bytes of data here and the most significant byte (MSB) comes out first.

In the code below, you can see that the process goes like this:

  1. Setup the SPI interface first given the settings discussed above and also set the clock speed as recommended by the manufacturers (It doesn’t seem to work if I don’t set this manually).
  2. Take the CS pin LOW and read out two bytes via SPI (MSB first). Then return the CS pin to HIGH again.
  3. Do some bit maths on these two bytes to convert them into a usable binary number.
  4. Cast this binary number into an int.
  5. Print the result to the serial window.
  6. Repeat.

Here’s some example raw data that we might read from the unit. The whole thing looks like this…

Format of data read from the ADC. Note that we read 16 bits but only 12 of them are actually useful. What follows in the code is an effort to shift things around and get rid of the junk bits in steps (1) to (4).

So what we need to do is keep only the data inside the box and get rid of everthing else. The key line in the code is this one…

ADCval = ((MSB & 0x3e)<<8 | LSB) >> 1;

Notice that we do a bitwise AND first with the MSB and 0x3e which is hex for 00011111 (step 1). This effectively does an AND operation with each bit in the MSB  and this number. Therefore anything in the first three bits gets switched to a 0 because any bit AND 0 -> 0. Then we shift the MSB left 8 places (step 2) and do a bitwise OR with the LSB (step 3). This effectively moves the MSB into its correct position as the most significant data and stuffs the LSB onto the end. Lastly, we shift everything right one place to get rid of the junk hanging off the end (step 4). This effectively shifts it off the end. Note that this will only work because we have declared ADCval as an unsigned int. If not, instead of a 0 being moved in from the left as we shift everything right, a 1 would end up there instead.

And here’s the rest of the code which includes the steps for addressing the ADC.

#include <SPI.h>
//pin connections
//Arduino       ADC
//12 MISO       6 (with 10k pull-up)
//10 CS         5
//13 SCK        7
const int CS = 8;

void setup() {
  // set the CS as an output:
  pinMode (CS, OUTPUT);
  Serial.begin(9600);     // opens serial port, sets data rate to 9600 bps
  
  SPI.begin();
  SPI.beginTransaction(SPISettings(20000, MSBFIRST, SPI_MODE0));
}

//function to read state of ADC
int ADCread(void)
{
  unsigned int ADCval=0;
  byte MSB,LSB;

  //now write to DAC
  // take the CS pin low to select the chip:
  digitalWrite(CS,LOW);
  delay(10);
  //  send in the address and value via SPI:
  
  MSB = SPI.transfer(0x00); //most sig byte
  LSB = SPI.transfer(0x00); //least sig byte

  //print out the raw measurement
  for (int i=7;i>=0;i--)
    Serial.print(bitRead(MSB,i));   
  for (int i=7;i>=0;i--)
    Serial.print(bitRead(LSB,i));
  Serial.println();

  //now get rid of the first three digits (most sig bits)
  //combine with the LSB
  //shift everything right one to get rid of junk least sig bit
  ADCval = ((MSB & 0x3e)<<8 | LSB) >> 1;

  //print out data again
  for (int i=15;i>=0;i--)
  Serial.print(bitRead(ADCval,i));
  Serial.println();
  
  delay(10);
  // take the CS pin high to de-select the chip:
  digitalWrite(CS,HIGH);

  return(ADCval);
}

void loop()
{
  int data = ADCread();
  Serial.println(data);
  delay(1000);
}

Here’s an example of the kind of thing that you get on the serial monitor…

1100110110101101
0000011001010110
1622
1100110110101010
0000011001010101
1621
1100110110101101
0000011001010110
1622
1100110110101101
0000011001010110
1622
1100110110101111
0000011001010111
1623
1100110110100111
0000011001010011
1619
1100011111001010
0000001101100101
869
1100010011110000
0000001001111000
632
1100000100101000
0000000000010100
20
1100000000000000
0000000000000000
0
1100000000000000
0000000000000000
0

…first, you get the raw data read from the ADC followed by the shifted bits after we do our maths. Then you get the decimal value representing the voltage which is Vin/Vref*4095.

I decided it might be a good idea to check the linearity of the device. I wanted to be able to relate the binary output from the ADC to a real voltage. Testing this is pretty easy.  I monitored the wiper terminal (ADC input) using a digital voltmeter and compared it to the ADC output from the serial window. Here is what I got…

As you can see, everything looks rosy. The response is linear to a high degree of accuracy and the gradient is 812 V^-1 = 4095 binary / 5.042V. I hope this post will help explain the basic process of how to implement an ADC. Get in touch if I’ve missed anything.

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?

}