Precision Flexinol Position Control Using Arduino


Flexinol, also known as Muscle Wire, is a strong, lightweight wire made from Nitinol that can be made to contract when conducting electricity. In this article I'll present an approach to precision control of this effect based on controlling the voltage in the Flexinol circuit. In addition, taking advantage of the fact that the resistance of Flexinol drops predictably as it contracts, the mechanism described here uses the wire itself as a sensor in a feedback control loop. Some advantages of eliminating the need for a separate sensor are reduced parts count and reduced mechanical complexity.

The information here follows on earlier discussions: Flexinol and other Nitinol Muscle Wires and Flexinol Control Circuit Using PIC 16F690 and ULN2003A. You might want to refer back to those articles for an introduction to using Flexinol in design and an overview of the properties of this unique alloy.

Built around an Arduino Uno, the setup I've put together to experiment with is very simple, and hopefully easily repeatable. To drive the Flexinol, it makes use of a homebrew digital to analog converter (DAC) that can be assembled from common parts. To calculate the resistance, the Arduino's native analog to digital (ADC) peripheral is used to measure the voltage drop across the Flexinol. The control code for the circuit first self-calibrates, and then steps through a set of targets attempting to hold positions across the range from fully relaxed to fully contracted. Throughout the run, data is sent to the Arduino serial monitor for observation and study.

Achieving precise, low-jitter position control of a Flexinol mechanism turns out to be surprisingly difficult. Although in robotics projects, Flexinol's contraction is usually driven by electricity, the shape memory effect that causes the contraction is actually a function of temperature. In an aptly named phenomenon called resistive heating, electric current makes the wire get hot and it contracts. All by itself, controlling resistive heating in a thin length of wire is elusive, involving variables such as ambient temperature, air flow, altitude and even humidity. Beyond those environmental basics however, controlling the Flexinol requires a reasonably thorough understanding how the wire will actually respond to changes in temperature.

In the on-off model of Flexinol control used in most hobby projects, we typically give the wire more than enough electric current to heat it and fully transition to the contracted Austenite phase. We then remove electricity entirely to allow the wire to cool and transition to the relaxed Martensite phase. Fortunately for the effort at hand, Flexinol does not simply transform at a particular temperature, but rather the effect is incremental across a rather wide temperature range. The full temperature span varies by the formulation but is typically between 40-70 degrees Celsius.

Adapted from the Flexinol specifications document, the above chart shows temperatures in Celsius plotted against the percentage of contraction for high temp Flexinol. Immediately apparent is the fact that the temperature response of Flexinol is non-linear. It begins gradually, accelerates to the point where most of the useful physical transformation takes place over about a 10 degree change in temperature, and then tapers off. A second observation is that there is a significant difference in the transformation temperatures when Flexinol is heating to the Austenite phase compared to when it is cooling to the Martensite phase. When heating, the mid-point between fully contracted and fully relaxed is at about 100 degrees; when cooling, the mid-point is about 70 degrees. This 30 degree gap is known as the transformation temperature hysteresis.

Also to be considered is the fact that when the current being supplied to the Flexinol is changed, the temperature doesn't change immediately, it needs time to either warm or cool. Therefore, when current is changed Flexinol will not move immediately to the new position, but will continue contracting or relaxing for a number of seconds. If we continue to change the current while the Flexinol is still settling, then we overshoot the target and the mechanism ultimately swings back and forth across the target rather than holding steady. Overshoot is further complicated by transformation hysteresis and heat dissipation issues. Typical approaches to feedback control where the current is proportionately increased or decreased until the desired position is reached won't work very well here.

In the solution presented here, transformation temperature hysteresis is accounted for by calibrating such that for a given position there is both a predetermined warming voltage value and a predetermined cooling voltage value. If the Flexinol is too relaxed to hold the target position then the warming value is fed to the DAC. If the Flexinol is too contracted for the position then the cooling value is fed to the DAC.

The warming and cooling values are determined in a calibration process each time the program is run. The first step in the calibration is to establish a baseline by measuring the resistance of the Flexinol when it is fully relaxed and fully contracted. In the second step, voltage to the Flexinol is steadily increased and then steadily decreased, all the while calculating the resistance of the Flexinol. When certain target resistance values are reached, the DAC values are stored in a lookup table. In making the calibrations, the current is changed at a moderate pace, a compromise between giving the Flexinol time to settle at the new position while still moving quickly enough to be useful. Although this approach does not completely eliminate overshoot, by sampling relatively continuously at high resolution and switching between warming and cooling current as needed, jitter in the mechanism can be reduced to trivial levels.

The Setup

Pictured nearby, the mechanical part of the experimental setup consists of a ~5.9" length of Flexinol attached to the short end of lever. When the Flexinol contracts the arm rocks upward; when it relaxes gravity acts as the bias force and stretches it back into shape. I'm using 0.005" diameter, high temperature Flexinol. According to the specifications, this wire has a resistance of 1.9 ohms/inch, a current of 320ma will cause it to fully contract in about 1 second, and it will relax in about 1.4 seconds with the current removed.

On the electronics side, current to the Flexinol is controlled by varying voltage. Because we'll be measuring resistance in the circuit, we need a nice clean voltage. To control the voltage I've setup a resistor ladder DAC (frequently written R-2R). An R-2R DAC is a clever construct that enables you to build up a DAC of theoretically any resolution simply by adding "rungs" to the ladder - 8 rungs equals an 8-bit DAC. Binary values are fed into the ladder by toggling eight digital pins from the Arduino, and a proportionately corresponding voltage is available at the output. The ladder is a resistor network built up of two values, one the double of the other. Here I've used 10K and 22K values, which it turns out is close enough. The other components of the DAC portion of the circuit are an LM358 operational amplifier and the PN2222 transistor. They are both setup as voltage followers in order to get enough current gain to supply the Flexinol. In my test circuit, I'm powering the Arduino through the USB programming cable and feeding the DAC with a separate 6 volt supply. Finally, the circuit feeds a voltage divider made up of a fixed value resistor (2.7 ohms) and the Flexinol which acts as a variable resistor.

Calculating the Resistance

In a voltage divider, when the value of the first resistor is known, the value of the second resistor can be calculated based on the voltage drop across the circuit using the following formula:

R2 = (R1 * VOut) / (VIn - VOut)

This calculation is pretty easy to code on the Arduino. The first step is to take readings using analogRead at A0 and A1. Those readings can then be converted to voltages by multiplying the values by 0.004883, which is equivalent to 5/1024 or reference voltage/ADC resolution.

Although the calculation itself is simple enough, it is here that we run into the limitations of both the test circuitry and the 10-bit ADC of the Arduino. According to the specifications the difference in resistance between Austenite and Martensite phase should be about 20%. Experimentally I've determined that in this particular circuit the actual useful range will be closer to 15% or about 1.5ohms for this length of wire. Therefore in order to produce useful results, measurements need to be accurate to within a tenth of an ohm, or better yet several hundredths. With this setup we just barely get there.

The Results

Although not perfect, the results of the experiment are quite good. According to data culled from test runs, the setup is able to reliably hold the Flexinol to a variance of <0.1ohm from the target value. Observing the mechanism while testing, there is occasional jitter when moving to a new position, but overall the arm is held solidly with no discernable movement. As it stands the technique could be applied in numerous projects where incremental positioning is needed, but extreme precision is not required. There are many opportunities for improvement, some of which are simply a matter of substituting higher quality components and building permanent rather than prototype circuitry. It seems likely that continued experimentation and refinement of the approach in both hardware and software will enable greater precision.

The Code

The sketch I used in this experiment is provided in its entirety below. or it can be downloaded as a zip file here: FlexinolResist.zip

Although there is probably limited value in reproducing the experiment exactly, the code is reasonably well commented and a carefully read should help make some of the notions described in the body of this article a little more clear.

/**************************************************************************************************
This sketch is part of an experiment in precise control of Flexinol (also known as Muscle Wire). 
The premise of this method is that because the resistance of Flexinol decreases as it contracts, 
the resistance at any given moment can be used to determine the current state of contraction.
Connected to a circuit and mechanism described at robotics.hobbizine.com/flexiposition.html, 
an Arduino is used to deliver a controlled voltage to a length of Flexinol while continuously
measuring the resistance.  The sketch spans two phases.  In the first phase the circuit is calibrated
and a set of voltage/resistance values is built.  In the second phase a method of using these
values to control the position of the Flexinol mechanism is tested.

Platform: Arduino Uno
Environment: Arduino 1.0
Full Description: robotics.hobbizine.com/flexiposition.html
***************************************************************************************************/


#include <math.h>

const float r1 = 2.7;   // Value of R1 in the Flexinol voltage divider
const byte msbPin = 9;  // The highest numbered pin of the 8 bit output to the DAC

float vIn;              // The measured voltage going into the Flexinol voltage divider
float vOut;             // The measured voltage between R1 and the Flexinol
float rFlexinol;        // The calculated resistance of the Flexinol

byte dac;               // The byte value to write to the DAC 
byte thisPin;           // Used in the routine to set the individual bits of the DAC
byte thisBit;           // Used in the routine to set the individual bits of the DAC

float avgActual;        // Used in testing to track the average resistance readings
float delta;            // Used in testing to track the difference between the target and actual values

int index;                    // Used to reference array variables
float resistanceValues[11];   // Target resistance values
int dacValuesWarming[11];     // Calibrated DAC values when Flexinol is warming  
int dacValuesCooling[11];     // Calibrated DAC values when Flexinol is cooling


/**************************************************************************************************
  Functional subroutines:

  bytewrite:  takes an 8-bit value and sends each bit to an individual Arduino pin
              to drive the DAC

  calcResist: takes ADC readings at A0 and A1 and uses the results to calculate the
              current resistance of the Flexinol
**************************************************************************************************/

void setup() {
  Serial.begin(9600);
  pinMode (2,  OUTPUT);
  pinMode (3,  OUTPUT);
  pinMode (4,  OUTPUT);
  pinMode (5,  OUTPUT);
  pinMode (6,  OUTPUT);
  pinMode (7,  OUTPUT);
  pinMode (8,  OUTPUT);
  pinMode (9,  OUTPUT);
}

void loop(){
  calibrate();
  test();
}

/**************************************************************************************************
  Calibration:

  This subroutine first measures the highest and lowest resistance readings across the range of
  potential voltages. It then calculates a set of resistance values at regular intervals.  Finally
  DAC values are cycled through continuously testing for the best value to achieve each of the
  calculated resistances.
**************************************************************************************************/

void calibrate(){
  Serial.println ("Working");
  Serial.println (" ");
  resistanceValues[0] = 7;   // Seed the high and low resistance values with values well outside
  resistanceValues[10] = 13; // the expected range
  dac = 0;
  byteWrite();
  delay(10000); 

/**************************************************************************************************
  Find the high and low resistance values and a corresponding DAC value
**************************************************************************************************/

  for (dac = 100 ; dac <= 210; dac++)   // Range determined experimentally
  {
    byteWrite();
    delay (500);
    calcResist();
    if (rFlexinol > resistanceValues[0])
    {
      resistanceValues[0] = rFlexinol;
      dacValuesWarming[0] = dac;
    } 
    if (rFlexinol < resistanceValues[10])
    {
      resistanceValues[10] = rFlexinol;
      dacValuesWarming[10] = dac;
    } 
  }

/**************************************************************************************************
  Intermediate resistance values (Flexinol positions) are calculated based on the high and low
  The results are stored in an array and output to the serial terminal
**************************************************************************************************/

  for (index = 0; index <= 10; index++)
  {
    resistanceValues[index] = ((resistanceValues[10] - resistanceValues[0]) * index * .1) + resistanceValues[0];
    Serial.print (index * 10, DEC);
    Serial.print ("% Contracted Resistance = ");
    Serial.println (resistanceValues[index], DEC);
  }

  Serial.println (" ");

/**************************************************************************************************
  Remove power and allow the Flexinol to cool
**************************************************************************************************/

  dac = 0;
  byteWrite();
  delay (3000);

/**************************************************************************************************
  Set values 1-9 in the warming values table to maximum to test against
**************************************************************************************************/

  for (index = 1; index <= 9; index++)
  {
    dacValuesWarming[index] = 255;
  }


/**************************************************************************************************
  Slowly increase voltage to the circuit - the first time a target resistance value is recorded
  place the corresponding DAC value in the warming index
**************************************************************************************************/

  dac = dacValuesWarming[0];
  do
  {
    byteWrite();
    delay(500);
    calcResist();

    for (index = 1; index <= 9; index++)
    {  
      if (rFlexinol <= resistanceValues[index] && dacValuesWarming[index] == 255)
      {
        dacValuesWarming[index] = dac - 1;
      }
    }
    ++dac;  
  }
  while (dacValuesWarming[9] == 255);   // repeat until all values mapped

/**************************************************************************************************
  Send the results of the warming calibration to the serial terminal
**************************************************************************************************/

  for (index = 0; index <= 10; index++)
  {
    Serial.print (index * 10, DEC);
    Serial.print ("% Contracted: DAC Value Warming = ");
    Serial.println (dacValuesWarming[index], DEC);
  }

  Serial.println (" "); 

/**************************************************************************************************
 The cooling calibration simply reverses the above warming calibration, steadily reducing the
 voltage and recording the DAC value the first time a target value is reached
 Note one difference: the cooling index checks to be sure that the cooling value is
 in fact less than the warming value, and if not keeps cycling 
**************************************************************************************************/


  for (index = 10; index >= 0; index--)
  {
    dacValuesCooling[index] = 0;
  }


  do
  {
    byteWrite();
    delay(500);
    calcResist();

    for (index = 0; index <= 10; index++)
    {  
      if (rFlexinol >= resistanceValues[index] && dacValuesCooling[index] == 0 && dac + 1 < dacValuesWarming[index])
      {
        dacValuesCooling[index] = dac + 1;
      }
    }
    --dac;  
  }
  while (dacValuesCooling[0] == 0 && dac > 100);

  for (index = 0; index <= 10; index++)
  {
    Serial.print (index * 10, DEC);
    Serial.print ("% Contracted: DAC Value Cooling = ");
    Serial.println (dacValuesCooling[index], DEC);
  }

}


/**************************************************************************************************
 Test

 The test routine is more straightforward than it may appear at first.  The circuit cycles through
 the target resistance values and attempts to hold the position.  Resistance is calculated.  If the
 Flexinol is too relaxed, the warming value is output to the DAC.  If the Flexinol is too
 contracted then the cooling value is used.

 The avgActual and delta variables and associated calculations are used to track average values
 and deviations from the target which are output to the terminal along with the other data.
**************************************************************************************************/

void test(){
  for (index = 1; index <= 9; index ++)
  {
    Serial.println (" ");
    Serial.print ("Testing "); 
    Serial.print (index * 10);
    Serial.println("%");


    dac = dacValuesWarming[index];
    for (int outerLoop = 0; outerLoop <= 4; outerLoop ++)
    {
      delta = 0;
      avgActual = 0;
      for (int innerLoop = 0; innerLoop <= 49; innerLoop ++)
      {
        byteWrite();
        delay (200);
        calcResist();
        avgActual = avgActual + rFlexinol;
        
        if (rFlexinol < resistanceValues[index])
        {
          dac = dacValuesCooling[index];   
        } 
        else {
          dac = dacValuesWarming[index];   
        }
        delta = delta + abs(resistanceValues[index] - rFlexinol);
      } 

      delta = delta /50;
      avgActual = avgActual /50;

      Serial.print ("Target: ");   
      Serial.print (resistanceValues[index], DEC);    
      Serial.print ("   Average Actual: ");
      Serial.print (avgActual, DEC);
      Serial.print ("   Delta: ");
      Serial.print (avgActual - resistanceValues[index], DEC);
      Serial.print ("   Average Deviation: ");
      Serial.println (delta, DEC);
    }  
  }
}


/**************************************************************************************************
  byteWrite takes the value in dac and outputs it to the 8-bit resistor ladder wired
  to Arduino pins 2-9.
**************************************************************************************************/

void byteWrite(){
  for (byte j = 0; j <= 7; j++){
    thisPin = msbPin - j;
    thisBit = bitRead(dac, 7 - j);
    digitalWrite (thisPin, thisBit);
  }  
}


/**************************************************************************************************
  calcResist takes ADC readings at A0 and A1, converts them to voltages and uses the results to
  calculate the resistance of the Flexinol - the constant value of 0.004883 used here is equal to
  the reference voltage of 5 divided by the ADC resolution of 1024
**************************************************************************************************/

void calcResist(){
  vIn = analogRead (A0);  // take a throw-away reading to reset the ADC after switching pins
  delay (10);
  vIn = analogRead (A0);
  vIn = vIn * 0.004883;
  
  vOut = analogRead (A1);
  delay (10);
  vOut = analogRead (A1);
  vOut = vOut * 0.004883;
  
  rFlexinol = (r1 * vOut) / (vIn - vOut);
}



Teach yourself the fundamentals of digital logic with the Deluxe Digital Logic Lab. The kit provides a generous assortment integrated circuits, transistors and other needed components. It is fully supported online at the Sully Station Technologies website with breadboard layouts, schematic diagrams, technical notes and datasheets. Build transistor logic gates, flip-flop memories, adders, counters, decoders, multiplexers and more.


Other Articles You Might Find Enjoyable

An Arduino Neural Network

Flexinol and other Nitinol Muscle Wires

Flexinol Control Circuit Using PIC 16F690 and ULN2003A

K'nexabeast - A Theo Jansen Style Octopod Robot

Design and Build Your Own Robot

Migrating to the 1284P

Getting Up and Running With a Tamiya Twin-Motor Gearbox


Things to Do Here

Home
Links








Privacy Policy