Coordinate scaling help

KPollock

Member
Seeking a bit of project help.

I'm using a circular touchpad in absolute mode (DIY touchpad joystick).

When I operate the touchpad it reports the x/y coordinates properly for me and I love it (even checked in windows game controller set-up).

My only issue is that when I release my finger the position goes back to 0,0 coordinates. I've tried to make it auto travel to 512, 512 (joystick center)
upon lifting my finger off but when that happens, I no longer am able to use the touchpad.

I'm curious if this is something that is indeed possible.
 
Can you give a bit more information? Your code, the part / library you're using etc...

The normal way to do something like this would be that the touchpad also has a position valid output (or for a touchpad with multi-touch, number of touches). You can then use this to set the value when the pad isn't in use.
 
My apologies but I don't know how to attach the code here. I can copy and paste the full thing if that's easier.
 
To paste code, press the # at the top of the post form, and paste your code between the CODE tags that appear.
 
Code:
// Current Prototype - March 2023
// Copyright (c) 2018 Cirque Corp. Restrictions apply. See: www.cirque.com/sw-license

#include <SPI.h>

// This device is currently built to operate with a Teensy 3.2. 
// It can easily be adapted to work with other Arduino-based systems. 
// This device is using a Cirque TM0XX0XX trackpad with a Curved Overlay. 
// A 12 pin FFC cable, A 12 pin 0.5mm Ribbon adapter, Jumper cables, and the Teensy

// This application connects to a TM0XX0XX circular trackpad via SPI. To verify that
// your trackpad is configured for SPI-mode, make sure that R1 is populated with a 
// 470k resistor (or whichever resitor connects pins 24 & 25 of the 1CA027 IC). 

// NOTE: To eliminate any confusion in the hookup process I've listed below the old
// and new terminology. For example MISO (OLD) = CIPO (NEW). Depending on when you
// started to code, this may be useful

// NOTE: Additionally I've color coded the wires to aid in your hookup process. 

// The TM0XX0XX circular trackpad is configured for Absolute mode tracking.
// The Teensy 3.2 becomes a USB joystick. You must select Joystick from the
// "tools > USB Type" menu. Joystick code pulled from public domain.  

// Pinnacle TM0XX0XX with Arduino (Hookup)
// Trackpad -> 12 pin FFC cable -> Ribbon Adapter -> Jumper cables -> Teensy 3.2

// 12 pin 0.5mm Ribbon Adapter
// Pin 1 SCK            (SPI Clock Line)                        (GREEN)
// Pin 2 MISO           (SPI Master Input Slave Output)         (RED)
//       CIPO           (SPI Controller In Peripheral Out)
// Pin 3 SS             (Slave Select)                          (BROWN)
//       CS             (Chip Select)
// Pin 4 DR             (Data Ready)                            (ORANGE)
// Pin 5 MOSI           (SPI Master Output Slave Input)         (PURPLE)
//       COPI           (SPI Controller out Peripheral In)
// Pin 6 BTN2           (Hardware input button #2)
// Pin 7 BTN3           (Hardware input button #3)
// Pin 8 BTN1           (Hardware input button #1)
// Pin 9 SCL            (I2C Clock Line)
// Pin 10 SDA           (I2C Data Line)
// Pin 11 GND           (Ground)                                (YELLOW)
// Pin 12 VDD           (3.3V Power Supply)                       (BLUE)

// Hardware pin-number labels (Teensy 3.2)
#define SCK_PIN       13    
#define DIN_PIN       12      
#define DOUT_PIN      11    
#define CS_PIN        10    
#define DR_PIN        9  

#define LED_0       21
#define LED_1       20

// Mask for Cirque Register Access Protocol (RAP)
#define WRITE_MASK  0x80
#define READ_MASK   0xA0

// Register config values for this demo
#define SYSCONFIG_1   0x00
#define FEEDCONFIG_1  0x83 
#define FEEDCONFIG_2  0x06 
#define Z_IDLE_COUNT  0x05

// Coordinate scaling values
#define PINNACLE_XMAX     2047    // max value Pinnacle can report for X
#define PINNACLE_YMAX     1535    // max value Pinnacle can report for Y
#define PINNACLE_X_LOWER  127     // min "reachable" X value
#define PINNACLE_X_UPPER  1919    // max "reachable" X value
#define PINNACLE_Y_LOWER  63      // min "reachable" Y value
#define PINNACLE_Y_UPPER  1471    // max "reachable" Y value
#define PINNACLE_X_RANGE  (PINNACLE_X_UPPER-PINNACLE_X_LOWER)
#define PINNACLE_Y_RANGE  (PINNACLE_Y_UPPER-PINNACLE_Y_LOWER)
#define ZONESCALE 256   // divisor for reducing x,y values to an array index for the LUT
#define ROWS_Y ((PINNACLE_YMAX + 1) / ZONESCALE)
#define COLS_X ((PINNACLE_XMAX + 1) / ZONESCALE)

// ADC-attenuation settings (held in BIT_7 and BIT_6)
// 1X = most sensitive, 4X = least sensitive
#define ADC_ATTENUATE_1X   0x00
#define ADC_ATTENUATE_2X   0x40
#define ADC_ATTENUATE_3X   0x80
#define ADC_ATTENUATE_4X   0xC0

// Convenient way to store and access measurements
typedef struct _absData
{
  uint16_t xValue;
  uint16_t yValue;
  uint16_t zValue;
  uint8_t buttonFlags;
  bool touchDown;
  bool hovering;
} absData_t;

absData_t touchData;

//const uint16_t ZONESCALE = 256;
//const uint16_t ROWS_Y = 6;
//const uint16_t COLS_X = 8;

// These values require tuning for optimal touch-response
// Each element represents the Z-value below which is considered "hovering" in that XY region of the sensor.
// The values present are not guaranteed to work for all HW configurations.
const uint8_t ZVALUE_MAP[ROWS_Y][COLS_X] =
{
  {0, 0,  0,  10,  10,  10, 0, 0},
  {0, 12,  23,  35,  35,  23, 12, 0},
  {0, 13,  35, 40, 40,  35, 13, 0},
  {0, 13,  35, 40, 40,  35, 13, 0},
  {0, 12,  23,  35,  35,  3, 12, 0},
  {0, 0,  0,  10,  10,  10, 0, 0},
};

// setup() gets called once at power-up, sets up serial debug output and Cirque's Pinnacle ASIC.
void setup()
{
  Serial.begin(115200);
  Joystick.useManualSend(true);

  pinMode(LED_0, OUTPUT);

  Pinnacle_Init();

  // These functions are required for use with thick overlays (curved)
  setAdcAttenuation(ADC_ATTENUATE_1X);
  tuneEdgeSensitivity();

  Serial.println();
  Serial.println("X\tY\tZ\tBtn\tData");
  Pinnacle_EnableFeed(true);
}

// loop() continuously checks to see if data-ready (DR) is high. If so, reads and reports touch data to terminal.
void loop()
{
  if(DR_Asserted())
  {
    Pinnacle_GetAbsolute(&touchData);
    Pinnacle_CheckValidTouch(&touchData);     // Checks for "hover" caused by curved overlays
    ScaleData(&touchData, PINNACLE_X_RANGE, PINNACLE_Y_RANGE);      // Scale coordinates to arbitrary X, Y resolution   

    Joystick.X(touchData.yValue);
    Joystick.Y(touchData.xValue);

    Serial.print(touchData.xValue);
    Serial.print('\t');
    Serial.print(touchData.yValue);
    Serial.print('\t');
    Serial.print(touchData.zValue);
    Serial.print('\t');
    Serial.print(touchData.buttonFlags);
    Serial.print('\t');
    if(Pinnacle_zIdlePacket(&touchData))
    {
      Serial.println("liftoff");
    }
    else if(touchData.hovering)
    {
      Serial.println("hovering");
    }
    else
    {
      Serial.println("valid");
    }
  }
  AssertSensorLED(touchData.touchDown);
  Joystick.send_now();
}

/*  Pinnacle-based TM0XX0XX Functions  */
void Pinnacle_Init()
{
  RAP_Init();
  DeAssert_CS();
  pinMode(DR_PIN, INPUT);

  // Host clears SW_CC flag
  Pinnacle_ClearFlags();

  // Host configures bits of registers 0x03 and 0x05
  RAP_Write(0x03, SYSCONFIG_1);
  RAP_Write(0x05, FEEDCONFIG_2);

  // Host enables preferred output mode (absolute)
  RAP_Write(0x04, FEEDCONFIG_1);

  // Host sets z-idle packet count to 5 (default is 30)
  RAP_Write(0x0A, Z_IDLE_COUNT);
  Serial.println("Pinnacle Initialized...");
}

// Reads XYZ data from Pinnacle registers 0x14 through 0x17
// Stores result in absData_t struct with xValue, yValue, and zValue members
void Pinnacle_GetAbsolute(absData_t * result)
{
  uint8_t data[6] = { 0,0,0,0,0,0 };
  RAP_ReadBytes(0x12, data, 6);

  Pinnacle_ClearFlags();

  result->buttonFlags = data[0] & 0x3F;
  result->xValue = data[2] | ((data[4] & 0x0F) << 8);
  result->yValue = data[3] | ((data[4] & 0xF0) << 4);
  result->zValue = data[5] & 0x3F;

  result->touchDown = result->xValue != 0;
}

// Checks touch data to see if it is a z-idle packet (all zeros)
bool Pinnacle_zIdlePacket(absData_t * data)
{
  return data->xValue == 0 && data->yValue == 0 && data->zValue == 0;
}

// Clears Status1 register flags (SW_CC and SW_DR)
void Pinnacle_ClearFlags()
{
  RAP_Write(0x02, 0x00);
  delayMicroseconds(50);
}

// Enables/Disables the feed
void Pinnacle_EnableFeed(bool feedEnable)
{
  uint8_t temp;

  RAP_ReadBytes(0x04, &temp, 1);  // Store contents of FeedConfig1 register

  if(feedEnable)
  {
    temp |= 0x01;                 // Set Feed Enable bit
    RAP_Write(0x04, temp);
  }
  else
  {
    temp &= ~0x01;                // Clear Feed Enable bit
    RAP_Write(0x04, temp);
  }
}


/*  Curved Overlay Functions  */
// Adjusts the feedback in the ADC, effectively attenuating the finger signal
// By default, the the signal is maximally attenuated (ADC_ATTENUATE_4X for use with thin, flat overlays
void setAdcAttenuation(uint8_t adcGain)
{
  uint8_t temp = 0x00;

  Serial.println();
  Serial.println("Setting ADC gain...");
  ERA_ReadBytes(0x0187, &temp, 1);
  temp &= 0x3F; // clear top two bits
  temp |= adcGain;
  ERA_WriteByte(0x0187, temp);
  ERA_ReadBytes(0x0187, &temp, 1);
  Serial.print("ADC gain set to:\t");
  Serial.print(temp &= 0xC0, HEX);
  switch(temp)
  {
    case ADC_ATTENUATE_1X:
      Serial.println(" (X/1)");
      break;
    case ADC_ATTENUATE_2X:
      Serial.println(" (X/2)");
      break;
    case ADC_ATTENUATE_3X:
      Serial.println(" (X/3)");
      break;
    case ADC_ATTENUATE_4X:
      Serial.println(" (X/4)");
      break;
    default:
      break;
  }
}

// Changes thresholds to improve detection of fingers
void tuneEdgeSensitivity()
{
  uint8_t temp = 0x00;

  Serial.println();
  Serial.println("Setting xAxis.WideZMin...");
  ERA_ReadBytes(0x0149, &temp, 1);
  Serial.print("Current value:\t");
  Serial.println(temp, HEX);
  ERA_WriteByte(0x0149,  0x04);
  ERA_ReadBytes(0x0149, &temp, 1);
  Serial.print("New value:\t");
  Serial.println(temp, HEX);

  Serial.println();
  Serial.println("Setting yAxis.WideZMin...");
  ERA_ReadBytes(0x0168, &temp, 1);
  Serial.print("Current value:\t");
  Serial.println(temp, HEX);
  ERA_WriteByte(0x0168,  0x03);
  ERA_ReadBytes(0x0168, &temp, 1);
  Serial.print("New value:\t");
  Serial.println(temp, HEX);
}

// This function identifies when a finger is "hovering" so your system can choose to ignore them.
// Explanation: Consider the response of the sensor to be flat across it's area. The Z-sensitivity of the sensor projects this area
// a short distance upwards above the surface of the sensor. Imagine it is a solid cylinder (wider than it is tall)
// in which a finger can be detected and tracked. Adding a curved overlay will cause a user's finger to dip deeper in the middle, and higher
// on the perimeter. If the sensitivity is tuned such that the sensing area projects to the highest part of the overlay, the lowest
// point will likely have excessive sensitivity. This means the sensor can detect a finger that isn't actually contacting the overlay in the shallower area.
// ZVALUE_MAP[][] stores a lookup table in which you can define the Z-value and XY position that is considered "hovering". Experimentation/tuning is required.
// NOTE: Z-value output decreases to 0 as you move your finger away from the sensor, and it's maximum value is 0x63 (6-bits).
void Pinnacle_CheckValidTouch(absData_t * touchData)
{
  uint32_t zone_x, zone_y;
  //eliminate hovering
  zone_x = touchData->xValue / ZONESCALE;
  zone_y = touchData->yValue / ZONESCALE;
  touchData->hovering = !(touchData->zValue > ZVALUE_MAP[zone_y][zone_x]);
}

/*  ERA (Extended Register Access) Functions  */
// Reads <count> bytes from an extended register at <address> (16-bit address),
// stores values in <*data>
void ERA_ReadBytes(uint16_t address, uint8_t * data, uint16_t count)
{
  uint8_t ERAControlValue = 0xFF;

  Pinnacle_EnableFeed(false); // Disable feed

  RAP_Write(0x1C, (uint8_t)(address >> 8));     // Send upper byte of ERA address
  RAP_Write(0x1D, (uint8_t)(address & 0x00FF)); // Send lower byte of ERA address

  for(uint16_t i = 0; i < count; i++)
  {
    RAP_Write(0x1E, 0x05);  // Signal ERA-read (auto-increment) to Pinnacle

    // Wait for status register 0x1E to clear
    do
    {
      RAP_ReadBytes(0x1E, &ERAControlValue, 1);
    } while(ERAControlValue != 0x00);

    RAP_ReadBytes(0x1B, data + i, 1);

    Pinnacle_ClearFlags();
  }
}

// Writes a byte, <data>, to an extended register at <address> (16-bit address)
void ERA_WriteByte(uint16_t address, uint8_t data)
{
  uint8_t ERAControlValue = 0xFF;

  Pinnacle_EnableFeed(false); // Disable feed

  RAP_Write(0x1B, data);    //Send data byte to be written

  RAP_Write(0x1C, (uint8_t)(address >> 8));     // Upper byte of ERA address
  RAP_Write(0x1D, (uint8_t)(address & 0x00FF)); // Lower byte of ERA address

  RAP_Write(0x1E, 0x05);  // Signal ERA-read (auto-increment) to Pinnacle

  // Wait for status register 0x1E to clear
  do
  {
    RAP_ReadBytes(0x1E, &ERAControlValue, 1);
  } while(ERAControlValue != 0x00);

  Pinnacle_ClearFlags();
}

/*  RAP Functions */

void RAP_Init()
{
  pinMode(CS_PIN, OUTPUT);
  SPI.begin();
}

// Reads <count> Pinnacle registers starting at <address>
void RAP_ReadBytes(byte address, byte * data, byte count)
{
  byte cmdByte = READ_MASK | address;   // Form the READ command byte

  SPI.beginTransaction(SPISettings(20000000, MSBFIRST, SPI_MODE1));

  Assert_CS();
  SPI.transfer(cmdByte);  // Signal a RAP-read operation starting at <address>
  SPI.transfer(0xFC);     // Filler byte
  SPI.transfer(0xFC);     // Filler byte
  for(byte i = 0; i < count; i++)
  {
    data[i] =  SPI.transfer(0xFC);  // Each subsequent SPI transfer gets another register's contents
  }
  DeAssert_CS();

  SPI.endTransaction();
}

// Writes single-byte <data> to <address>
void RAP_Write(byte address, byte data)
{
  byte cmdByte = WRITE_MASK | address;  // Form the WRITE command byte

  SPI.beginTransaction(SPISettings(10000000, MSBFIRST, SPI_MODE1));

  Assert_CS();
  SPI.transfer(cmdByte);  // Signal a write to register at <address>
  SPI.transfer(data);    // Send <value> to be written to register
  DeAssert_CS();

  SPI.endTransaction();
}

/*  Logical Scaling Functions */
// Clips raw coordinates to "reachable" window of sensor
// NOTE: values outside this window can only appear as a result of noise
void ClipCoordinates(absData_t * coordinates)
{
  if(coordinates->xValue < PINNACLE_X_LOWER)
  {
    coordinates->xValue = PINNACLE_X_LOWER;
  }
  else if(coordinates->xValue > PINNACLE_X_UPPER)
  {
    coordinates->xValue = PINNACLE_X_UPPER;
  }
  if(coordinates->yValue < PINNACLE_Y_LOWER)
  {
    coordinates->yValue = PINNACLE_Y_LOWER;
  }
  else if(coordinates->yValue > PINNACLE_Y_UPPER)
  {
    coordinates->yValue = PINNACLE_Y_UPPER;
  }
}

// Scales data to desired X & Y resolution
void ScaleData(absData_t * coordinates, uint16_t xValue, uint16_t yValue)
{
  uint32_t xTemp = 0;
  uint32_t yTemp = 0;

  ClipCoordinates(coordinates);

  xTemp = coordinates->xValue;
  yTemp = coordinates->yValue;

  // translate coordinates to (0, 0) reference by subtracting edge-offset
  xTemp -= PINNACLE_X_LOWER;
  yTemp -= PINNACLE_Y_LOWER;

  // scale coordinates to (xResolution, yResolution) range
  coordinates->xValue = (uint16_t)(xTemp * xValue / PINNACLE_X_RANGE);
  coordinates->yValue = (uint16_t)(yTemp * yValue / PINNACLE_Y_RANGE);
}

/*  I/O Functions */
void Assert_CS()
{
  digitalWrite(CS_PIN, LOW);
}

void DeAssert_CS()
{
  digitalWrite(CS_PIN, HIGH);
}

void AssertSensorLED(bool state)
{
  digitalWrite(LED_0, !state);
}

bool DR_Asserted()
{
  return digitalRead(DR_PIN);
}
 
I think you could get away with something as simple as this:

Code:
void loop()
{
  if(DR_Asserted())
  {
    Pinnacle_GetAbsolute(&touchData);
    Pinnacle_CheckValidTouch(&touchData);     // Checks for "hover" caused by curved overlays
    if (touchData.touchDown) 
      ScaleData(&touchData, PINNACLE_X_RANGE, PINNACLE_Y_RANGE);      // Scale coordinates to arbitrary X, Y resolution   
    else {
      touchData.xValue = PINNACLE_X_RANGE/2;
      touchData.yValue = PINNACLE_Y_RANGE/2;
    }
    ....

(edit to change the no touch values to middle of the range rather than a hard coded 512)
 
That was able to get it to work. I very much appreciate your help.

Though I needed to change PINNACLE_X_RANGE/2 & PINNACLE_Y_RANGE/2 to 512 for both.
The coordinates in the end were pulling to the bottom right. But it's all good now.

Again, thank you for your time, and help. :)
 
I'm planning on making a YouTube video on this so people can make their own touchpad joystick. AndyA can I have permission to give you a shoutout and thank you there too.
 
I'm planning on making a YouTube video on this so people can make their own touchpad joystick. AndyA can I have permission to give you a shoutout and thank you there too.

You can but I don't think it's very deserved, I'm sure lots of people could have told you the same thing. It would probably be better to simply thank the community / people on the forum rather than me specifically.

Also - with regards to setting it to 512 as standard, that would imply that your normal results are very slightly off and that you're not getting the full 0 to 1024 range on each axis. You may want to look at doing something along the lines of x = x * 1024 / PINNACLE_X_RANGE to scale to the correct size.
 
Back
Top