Skip to content

QTouch Capacitive Rotary Sensors Made Easy

December 3, 2011

Well, at least in Eagle…

Sensor Design

If you want to use a capacitive touch interface in your AVR design, you’re either slogging through it for the learning experience, using a library from somebody who’s already done the slogging, or you’re using the Atmel QTouch libraries.

In particular, if you’re looking to do the rotary encoders, like the one on page 52 in the QTouch Sensor Design Guide, you will find yourself with a bit of an issue: they’re very difficult to draw.

2011-12-03 23h42_36

Yep, large, curved polygonal regions with close gapping. Not fun. The only way to really do this correctly is to make it happen with software.

ULP Solution

So here you go, an Eagle ULP that draws these things:

#usage "<qt><nobr>This ULP calculates and place a rotary capacitive sensor.<p>"
       "<author>Author: Dan Afonso <dafonso@afonsoconsulting.com>, </author></nobr></qt>";
// THIS PROGRAM IS PROVIDED AS IS AND WITHOUT WARRANTY OF ANY KIND,
// EXPRESSED OR IMPLIED.
//Globals
string solderpoint      = "";
string file;
string s;  //temporary string
int c; // temporary counter
// Basic parameters
real outerRadius        = 30.0;
real innerRadius        = 15.0;
real bandMaximum        = 5.0;
int channels            = 3;
real channelGap            = 1;
real channelSpacing        = 4.5;
int debug                = 1;
real rad2deg            = 360/(2*PI);
// Converts from radial coordinates to Cartesian. This returns the X portion
real r2c_x(real r, real a, real o){
    return (r*cos(a)) + (o*sin(a));
}
// Converts from radial coordinates to Cartesian. This returns the Y portion
real r2c_y(real r, real a, real o){
    return (r*sin(a)) - (o*cos(a));
}
// The actual work.
void doIt(void){
    // Setup
    string cmd = "CHANGE WIDTH .5;\nGRID mm;SET WIRE_BEND 2;\n";
    real channelAngles[];

    // Figure out how wide our bands are going to be
    int bands = ceil((outerRadius - innerRadius)/bandMaximum);
    real bandWidth = (outerRadius - innerRadius)/real(bands);
    sprintf(s, "# Bands: %d, Band width: %.4f\n", bands, bandWidth);
    cmd += s;

    // Draw the tDocu layer

    sprintf( s, "Change layer %d;\n", 51);
    cmd += s;
    sprintf(s, "CIRCLE (0 0) (%.4f 0);\n", outerRadius);
    cmd += s;
    sprintf(s, "CIRCLE (0 0) (%.4f 0);\n", innerRadius);
    cmd += s;
    sprintf(s, "CHANGE WIDTH .1;\n");
    cmd += s;

    // Get our angles for the channels

    for (c = 0; c < channels; c++){
        channelAngles[c] = (c*2*PI)/real(channels);

        sprintf(s, "WIRE (%.4f %.4f) (%.4f %.4f);\n",
            r2c_x(innerRadius, channelAngles[c], 0), r2c_y(innerRadius, channelAngles[c], 0),
            r2c_x(outerRadius, channelAngles[c], 0), r2c_y(outerRadius, channelAngles[c], 0));
        cmd += s;
        sprintf(s, "WIRE (%.4f %.4f) (%.4f %.4f);\n",
            r2c_x(innerRadius, channelAngles[c], channelSpacing/2), r2c_y(innerRadius, channelAngles[c], channelSpacing/2),
            r2c_x(outerRadius, channelAngles[c], channelSpacing/2), r2c_y(outerRadius, channelAngles[c], channelSpacing/2));
        cmd += s;
        sprintf(s, "WIRE (%.4f %.4f) (%.4f %.4f);\n",
            r2c_x(innerRadius, channelAngles[c], -1*channelSpacing/2), r2c_y(innerRadius, channelAngles[c], -1*channelSpacing/2),
            r2c_x(outerRadius, channelAngles[c], -1*channelSpacing/2), r2c_y(outerRadius, channelAngles[c], -1*channelSpacing/2));
        cmd += s;

    }

    // and add band markings
    for (c = 0; c < bands; c++){
        sprintf(s, "CIRCLE (0 0) (%.4f 0);\n", innerRadius + c * bandWidth);
        if (c > 0) cmd += s;       
    }

    // OK, now to draw the polygons
    cmd += "CHANGE LAYER 1;\n";
    cmd += "CHANGE WIDTH .1;\n";
    real gap = channelGap / 2;
    real spacing = channelSpacing / 2;
    real arcAngle = channelAngles[1] * rad2deg;
    for (c = 0; c < channels; c++){
        // Add some clarity by defining these
        real channelAngleStart = channelAngles[channels - 1];
        if (c > 0) channelAngleStart = channelAngles[c - 1];
        real channelAngleStop = channelAngles[0];
        if (c < channels) channelAngleStop = channelAngles[c + 1];
        sprintf(s, "# Channel %d. %d Bands. Starts at (%.2f, %.1f) (with offsets) and goes from %.1f degrees to %.1f degrees\n",
            c + 1, bands, innerRadius, channelAngles[c] * rad2deg,  channelAngleStart * rad2deg, channelAngleStop * rad2deg);
        cmd += s;
        sprintf(s, "POLYGON (%.4f %.4f) -%2f ",
            r2c_x(innerRadius, channelAngles[c], /*gap - spacing*/0), r2c_y(innerRadius, channelAngles[c], /*gap - spacing*/0), arcAngle);
        cmd += s;
        for (int b = 0; b < bands; b++){
            real bandStart = innerRadius + (b * bandWidth);
            real bandEnd = bandStart + bandWidth;
            real bandMid = bandStart + (bandWidth / 2);
            // Draw the band
            sprintf(s, "(%.4f %.4f) +%.2f (%.4f %.4f) (%.4f %.4f) -%2f ",
                r2c_x(bandStart, channelAngleStart, 0 - spacing - gap), r2c_y(bandStart, channelAngleStart, 0 - spacing - gap), arcAngle,
                r2c_x(bandMid - gap, channelAngles[c], spacing - gap), r2c_y(bandMid - gap, channelAngles[c], spacing - gap),
                r2c_x(bandMid + gap, channelAngles[c], spacing - gap), r2c_y(bandMid + gap, channelAngles[c], spacing - gap),
                arcAngle);
            cmd += s;
        }

        // bring it around to the other side
        sprintf(s, "(%.4f %.4f) +%.2f (%.4f %.4f) +%.2f ",
            r2c_x(outerRadius + gap, channelAngleStart, 0 - gap - spacing), r2c_y(outerRadius + gap, channelAngleStart, 0 - gap - spacing),
            arcAngle,
            r2c_x(outerRadius, channelAngles[c], gap - spacing), r2c_y(outerRadius, channelAngles[c], gap - spacing),
            arcAngle
            );
        cmd += s;

        // Work our way in
        for (b = bands - 1; b >= 0; b--){
            real bandStart = innerRadius + (b * bandWidth);
            real bandMid = bandStart + (bandWidth / 2);
            // Draw the band
            sprintf(s, "(%.4f %.4f) -%.2f (%.4f %.4f) (%.4f %.4f)  +%.2f ",
                r2c_x(bandMid, channelAngleStop, gap + spacing), r2c_y(bandMid, channelAngleStop, gap + spacing), arcAngle,
                r2c_x(bandStart + gap, channelAngles[c], 0 - spacing + gap), r2c_y(bandStart + gap, channelAngles[c], 0 - spacing + gap),
                r2c_x(bandStart - gap, channelAngles[c], 0 - spacing + gap), r2c_y(bandStart - gap, channelAngles[c], 0 - spacing + gap), arcAngle
                );
            cmd += s;
        }
        // Close the arc
        sprintf(s, "(%.4f %.4f) ",
            r2c_x(innerRadius, channelAngles[c], /*gap - spacing*/0), r2c_y(innerRadius, channelAngles[c], /*gap - spacing*/0));
        cmd += s;
        cmd += ";\n";
    }

    // Add the pads
    for (c = 0; c < channels; c++){
        sprintf(s, "PAD 1.6764 round 'p%d' (%.4f %.4f)\n",
            c,
            r2c_x(innerRadius + 1, channelAngles[c], 0), r2c_y(innerRadius + 1, channelAngles[c], 0));
        cmd +=s;
    }
    // Finish up
    output(file, "wtD") printf("%s", cmd);
    sprintf(cmd, "SCRIPT '%s';", file);
    exit (cmd);
}
// *** main ***
// Setup
if (board) {
  board(B) file = filesetext(B.name, ".scr");
  solderpoint = ";\nVIA ";
}
else if (package) {
  library(L) file = filesetext(L.name, ".scr");
  solderpoint = ";\nPAD ";
}
else {
  dlgMessageBox("Start this ULP in a Board- or Package-Editor!", "OK");
  exit(0);
}
// Dialog
/*real bandMinimum        = 5.0;
real bandMaximum        = 8.0;
int channels            = 3;
real channelGap            = .1;
int debug                = 1;
*/
dlgDialog(filename(argv[0])) {
    dlgVBoxLayout {
        dlgLabel("<nobr> All measures in <b>mm</b> </nobr>");
        dlgGridLayout {
            dlgCell(0,0) dlgLabel("Outer Raidus");
            dlgCell(0,1) dlgRealEdit(outerRadius, 20.0, 100.0);
            dlgCell(1,0) dlgLabel("Inner Raidus");
            dlgCell(1,1) dlgRealEdit(innerRadius, 5.0, 30.0);
        }
    }
    dlgHBoxLayout {
        dlgPushButton("OK") {
            int ok = 1;

            if (innerRadius > (outerRadius - bandMinimum)){
                ok = 0;
                sprintf(s, "<qt>Inner Radius is too large");
                dlgMessageBox(s, "OK");
            }
            if (ok) {
                dlgAccept();
                doIt();
            }
        }
    }
};

Just run it and fill out the form while you’re in a library and it will do the dirty work. If the bits get too thin, it may drop some invalid polygon warnings. Be aware that you might need to manually fix these, or ignore them.

2011-12-03 23h52_20

Potential Improvements

As I see it, there are some potential improvements to be made:

  • The curve just uses a 120° arc, not the correct arc length as computer with the gaps. This leads to the odd bowing you see that becomes much clearer at smaller sizes.
  • The outer ring needs a little indentation put in so that you can fit the curves a bit better.
  • Option to use the alternate layout that Atmel recommends. The alternate layout is not symmetric, but does bring all three regions to the same place for easier routing .

Photo credits: QT600 kit, and sensor layout belong to Atmel, and I claim no ownership. Please don’t sue me.

Advertisements

From → Hardware

Leave a Comment

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s