Part 4: Arduino Code

With the fundamentals covered and hardware assembled, it's coding time.

Here's an example Arduino sketch that reads an XYZ pad and outputs position + force readings in a serial terminal. You can grab the .zip below and upload to your MCU, or just take a quick glance for reference if you'd prefer to write your own.

Download Code: XYZPadTutorial.zip

Code:
/**********************************************************************************************************
* Project: XYZPadTutorial.ino
* By: Chris Wittmier @ Sensitronics LLC
* LastRev: 03/29/2015
* Description: Demo Firmware for FSR-based XYZPad sensors (Similar to 4-wire resistive touchscreen + force)
**********************************************************************************************************/
#include
#include "xyzdefines.h"



/**********************************************************************************************************
* GLOBALS
**********************************************************************************************************/
int history[AXIS_COUNT][AVERAGE_COUNT]; //rolling history of multiple readings for averaging
int average[AXIS_COUNT]; //the current averaged value for each axis

// Offset and scaling are used to map position data to ideally output fullscale readings from edge to edge
// of the Pad area. If you are using the Processing demo application, there are better controls there, so
// these are unneccesary.

float offset[AXIS_COUNT];
float scale[AXIS_COUNT];

int history_index;


/**********************************************************************************************************
* setup()
**********************************************************************************************************/
void setup()
{
Serial.begin(115200);

pinMode(PIN_XPLUS, INPUT); //init all pins to Hi-Z
pinMode(PIN_XMINUS, INPUT);
pinMode(PIN_YPLUS, INPUT);
pinMode(PIN_YMINUS, INPUT);

for(int i = 0; i < AXIS_COUNT; i ++)
{
for(int k = 0; k < AVERAGE_COUNT; k ++)
{
history[i][k] = 0; //clear history
}
average[i] = 0;
offset[i] = 0; //default all position offsets to 0
scale[i] = 1; //default all position scaling to 1:1
}

history_index = 0;
}


/**********************************************************************************************************
* loop() - Note that some of the pinModes / digitalWrites below are extraneous (same as the previous state).
* However, we'll list them all for the sake of example clarity.
**********************************************************************************************************/
void loop()
{

/* Read X-position */

pinMode(PIN_YPLUS, INPUT);
pinMode(PIN_YMINUS, INPUT);
pinMode(PIN_XPLUS, OUTPUT);
pinMode(PIN_XMINUS, OUTPUT);
digitalWrite(PIN_XMINUS, LOW); //To read X position, put 5V-0V across the X-plane...
digitalWrite(PIN_XPLUS, HIGH);
analogRead(PIN_YPLUS); // (dummy read gives input multiplexer time to settle to avoid crosstalk)
int x_reading = analogRead(PIN_YPLUS); //...then read voltage from either Y-terminal
digitalWrite(PIN_XPLUS, LOW);


/* Read Y-position */

pinMode(PIN_XPLUS, INPUT);
pinMode(PIN_XMINUS, INPUT);
pinMode(PIN_YPLUS, OUTPUT);
pinMode(PIN_YMINUS, OUTPUT);
digitalWrite(PIN_YMINUS, LOW); //Same idea as last state, but switch axes
digitalWrite(PIN_YPLUS, HIGH);
analogRead(PIN_XPLUS);
int y_reading = analogRead(PIN_XPLUS);
digitalWrite(PIN_YPLUS, LOW);


/* Read Force */

pinMode(PIN_XPLUS, INPUT);
pinMode(PIN_YPLUS, INPUT);
pinMode(PIN_XMINUS, OUTPUT);
pinMode(PIN_YMINUS, OUTPUT);
digitalWrite(PIN_XMINUS, LOW); //To read force, put 5V-0V from one Y-terminal to one X-terminal...
digitalWrite(PIN_YMINUS, HIGH);
analogRead(PIN_XPLUS);
int x_plane_reading = analogRead(PIN_XPLUS); //...read the opposite X-terminal to get voltage at the X-plane...
analogRead(PIN_YPLUS);
int y_plane_reading = analogRead(PIN_YPLUS); //...read the opposite Y-terminal to get voltage at the Y-plane...
digitalWrite(PIN_YMINUS, LOW);

int z_reading = y_plane_reading - x_plane_reading; //...now subtract to find voltage between the two planes, proportional to resistance between sheets, and thus relative force


/* Idle */

pinMode(PIN_XPLUS, INPUT); //set everything to Hi-Z in case we do some other tasks at this point, no reason to keep driving current through the sheet
pinMode(PIN_XMINUS, INPUT);
pinMode(PIN_YPLUS, INPUT);
pinMode(PIN_YMINUS, INPUT);

if(FLIP_X_AXIS)
{
x_reading = 1024 - x_reading;
}
if(FLIP_Y_AXIS)
{
y_reading = 1024 - y_reading;
}
if(FLIP_Z_AXIS)
{
z_reading = 1024 - z_reading;
}

averageAll(x_reading, y_reading, z_reading); //smoothing

#ifdef TERMINAL_OUTPUT
if(average[Z_AXIS] >= Z_CONTACT_THRESHOLD)
{
Serial.print("X: ");
printFixed(average[X_AXIS], 4);
Serial.print(" Y: ");
printFixed(average[Y_AXIS], 4);
Serial.print(" Z: ");
printFixed(average[Z_AXIS], 4);
Serial.println();
}
#endif

#ifdef PROCESSING_OUTPUT
procReport(average[X_AXIS], average[Y_AXIS], average[Z_AXIS]);
#endif

delay(DELAY_AFTER_SEND);

}


/**********************************************************************************************************
* averageAll() - throw singular readings from this loop into global history array, then average values
* in the array to get current average for each axis, store in global average array
**********************************************************************************************************/
void averageAll(int xvalue, int yvalue, int zvalue)
{
unsigned long x_sum = 0;
unsigned long y_sum = 0;
unsigned long z_sum = 0;

history[X_AXIS][history_index] = xvalue;
history[Y_AXIS][history_index] = yvalue;
history[Z_AXIS][history_index] = zvalue;

for(int i = 0; i < AVERAGE_COUNT; i ++)
{
x_sum += history[X_AXIS][i];
y_sum += history[Y_AXIS][i];
z_sum += history[Z_AXIS][i];
}

average[X_AXIS] = (int)(x_sum / AVERAGE_COUNT);
average[Y_AXIS] = (int)(y_sum / AVERAGE_COUNT);
average[Z_AXIS] = (int)(z_sum / AVERAGE_COUNT);

history_index = (history_index < (AVERAGE_COUNT - 1)) ? history_index + 1 : 0; //increment history index

}


/**********************************************************************************************************
* procReport() - output packet formatted for Processing graphical demo
* Packet is formatted:
* 0: X-HIGH
* 1: X-LOW
* 2: Y-HIGH
* 3: Y-LOW
* 4: Z-HIGH
* 5: Z-LOW
* 6: 0xFF <-- end markers to keep synced in case the serial port misses a byte
* 7: 0xFF
**********************************************************************************************************/
void procReport(int xvalue, int yvalue, int zvalue)
{
float temp;
byte buff[8];

temp = ((float) xvalue - offset[X_AXIS]) * scale[X_AXIS]; //scale and sanitize X
if(temp > 1023)
{
xvalue = 1023;
}
else if(temp < 0)
{
xvalue = 0;
}
else
{
xvalue = (int) temp;
}


temp = ((float)yvalue - offset[Y_AXIS]) * scale[Y_AXIS];
if(temp > 1023)
{
yvalue = 1023;
}
else if(temp < 0)
{
yvalue = 0;
}
else
{
yvalue = (int) temp;
}

if(zvalue >= Z_CONTACT_THRESHOLD)
{
buff[0] = (highByte(xvalue));
buff[1] = (lowByte(xvalue));
buff[2] = (highByte(yvalue));
buff[3] = (lowByte(yvalue));
buff[4] = (highByte(zvalue));
buff[5] = (lowByte(zvalue));
buff[6] = (0xFF);
buff[7] = (0xFF);
Serial.write(buff, 8);
}

}


/**********************************************************************************************************
* printFixed() - print an integer in the terminal with fixed places, pad with spaces, to keep values in
* readable columns as terminal scrolls.
* places value should be chosen to accommodate all digits of largest expected value, +1 if signed
**********************************************************************************************************/
void printFixed(int value, int places)
{
int temp = 1;
if(value < 0)
{
places -= 1;
}
for(int i = 1; i < places; i++)
{
temp *= 10;
if(value < temp)
{
Serial.print(" ");
}
}
Serial.print(value);
}






And here's the header file for that sketch (it's also included in the project .zip file posted above). The readily tweak-able parameters are in here.

Code:
/**********************************************************************************************************
* File: xyzdefines.h
* By: Chris Wittmier @ Sensitronics LLC
* LastRev: 03/29/2015
* Description: Global macros / pin defs / operation parameters for XYZPadTutorial
**********************************************************************************************************/
#ifndef xyzdefines_h
#define xyzdefines_h


/**********************************************************************************************************
* PIN MAPPING / ADJUSTABLE PARAMS
**********************************************************************************************************/
#define TERMINAL_OUTPUT 1 //if defined, output readings as terminal text ("X, Y, Z \n")
//#define PROCESSING_OUTPUT 1 //if defined, format packet to communicate with Processing sketch (graphic output)

#define PIN_XPLUS A0
#define PIN_XMINUS A1
#define PIN_YPLUS A2
#define PIN_YMINUS A3

#define Z_CONTACT_THRESHOLD 5 //below force threshold, no output happens - without this threshold, floating inputs will result in bogus position reports
#define DELAY_AFTER_SEND 1 //msec to wait between reports, otherwise the Arduino terminal window gets all crashy
#define AVERAGE_COUNT 5 //consecutive samples to average to achieve very basic smoothing filter

#define FLIP_X_AXIS 0
#define FLIP_Y_AXIS 0
#define FLIP_Z_AXIS 1


/**********************************************************************************************************
* GLOBAL MACROS
**********************************************************************************************************/
#define AXIS_COUNT 3

#define X_AXIS 0
#define Y_AXIS 1
#define Z_AXIS 2




#endif

Sample Output


If you've uploaded the example firmware and opened a terminal window at 115200kbps you should see... a blank terminal.

This is because there's a threshold in the code which blocks output until force is detected. Without a threshold, you'd see a stream of false position data; the ADC inputs are more or less floating when the sensor layers are not pressed together.

So give it a press, and you should see something like the screenshot at right.


If you've oriented things similarly, pressing the top left corner should show low X and Y values, which gradually increase as you move right and down, respectively.


If not, give all the hardware connections a double check to verify everything's in order.


Graphics Would Be Nice


If everything went successfully, you now have a functional XYZ pad and some terminal output. We could call it a day, but let's (optionally) take it one step further!



In the final section, we'll add graphical feedback with a simple but rather neat demo GUI, using Processing...