Introduction

This training will show how to use the Sensor Controller (SC) to do Capacitive Sensing and how to use the run time logging. Capasitive sensing is used in applications where you need touch capabilities but can't have physical buttons or a metallic touch surface. A typical user case can be a panel with buttons and sliders implemented as part of the PCB that is completely incapsulated in plastic which can be mounted in exposed enviorements and reduce cost.

The data will be displayed as graphs using the Run-Time Logging functionality in Sensor Controller Studio (SCS).

The measured capacitance will change as a finger touches the surface. It is this change that we can detect as touch. We can measure capacitance with the time-to-digital converter (TDC), combined with the ISRC (constant configurable current source) and COMPA peripherals which are used to control the TDC. To achieve good noise suppression, the reference start at 0.6 V by connecting a 400 kohm resistor to ground to the REF node (not shown in the figure). When the input reaches the first reference level, the comparator output goes high and the REF node is pulled to VDDS for 1 us to force the node high fast. The output of the comparator will then go low. At the same time the 400 kohm resistor is disconnected. Next the reference level is set to 2.4V, and the comparator will again go high when the input has reached this new reference level. At this point we can read out a 24-bit integer from the TDC, which provides the number of clock edges between the two positive edges of the comparator output. This value is a measure of the capacitance, in terms of the time constant of an RC circuit. We don't actually care about the absolute capacitance, but rather a relative measure

Reference voltage

The reference voltages is selected according to the supply voltage on a Launchpad. For systems that could have a lower supply voltage it could be required to adjust the reference voltage if the supply voltage is lower than 3.3 V.

The point where the TDC starts to count is given as Rext || 400 kohm x 2 uA. The point where the TDC stops to count is given as Rext x 2 uA where Rext is the external resistor. This voltage should be at least 0.5 V lower than VDDS:

Compatible SimpleLink MCU LaunchPad kits

This workshop can be completed with any one of the SimpleLink™ Wireless MCU with Sensor Controller devices described in the table below. Install the required Associated SimpleLink Software Development Kit matching your device. More details on LaunchPads please visit the LaunchPad overview page.

Prerequisites

Software for desktop development

  • SCS 2.5.0 or newer. You can find version info in the menu Help->About Sensor Controller Studio

Hardware

The picture below shows the BOOSTXL-ULPSENSE with emphasizes on the two capacitive touch buttons.

Task 0 – Open the SCS Project

  1. Start SCS and locate the Start Page
  2. Under Examples, double click on "Capacitive Touch" under "LaunchPad + ULP Sense BoosterPack"
  3. Make sure that TI-RTOS is chosen as the Operating System and that the chip you are using is chosen as the Target Chip Name

You should now have something similar to this.

Connect your LaunchPad with the USB cable to the computer. Connect the BOOSTXL-ULPSENSE on top of the LaunchPad. The two capacitive sensing buttons are connected to DIO26 and DIO27.

Task Resources

All resources are already setup in the example and no extra steps are required to run the example. This section goes through the resources used in the example. To view the resources used, go to Task Resources. This can be accessed by clicking the task name in the project view.

The following resources are needed:

  • Analog Open-Drain Pins
  • Analog Pins (see "Deciding Capacitive Touch Input Pins" below)
  • COMPA
  • ISRC
  • TDC
  • Multi-buffered Output Data Exchange, "Buffer Count=3" and "Prevent overflow at buffer switch = enable", everything else disabled
  • Peripheral Sharing
  • System CPU Alert
  • Timer Event Trigger
  • Delay Insertion
  • Math and Logic

The Sensor Controller Task is executed at a dynamic interval. This is done by using Timer 1 to trigger Event Handler A Code.

This example uses two capacitive touch buttons.

The device has 8 analog pins, but one of them are used for an external resistor for the internal current reference, which means up to 7 pins are available for capacitive buttons. If the requirement is to use a different number of buttons than used in this example , go to "Analog Open-Drain Pins" and the line "Pin count 2". Change this to something between 1 and 7. The "Pin count" under "Analog Pins" is for the reference resistor. Keep its default value 1.

I/O Mapping

To see which pins are used by the example or change the pin mapping, go to "I/O mapping".

Depeding on how many capacitive sensing inputs selected, the I/O Mapping will differ. A list of compatible input pins are shown below. These are the pins that are described as "analog" in the datasheet.

  • DIO23
  • DIO24
  • DIO25
  • DIO26
  • DIO27
  • DIO28
  • DIO29
  • DIO30

When using the ULPSENSE boosterpack, the reference resistor is mapped to DIO25, and the capacitive buttons to DIO27 and DIO28 respectively. For a custom product the resistor and capacitive button elements can be mapped to any of the above listed pins.

Available pins

Some CC13xx Launchpads use DIO28, DIO29 and DIO30 for control signals to other RF circuits on the Launchpad. The "board mapping" list in the lower part of the I/O Mapping window gives information if the pin is available or not.

Task 1 – Testing The Code Using The Run-Time Logging Tool

Task 1.1 - Get to know the GUI

Click the Run-Time Logging in the menu to the left. Connect to it by pressing the connect symbol at the top, or press F12. When the LaunchPad has successfully connected, click the Run symbol or press F5 in order to start the logging.

You can hover the mouse over each button to see a short description.

  1. Connect
  2. Restart
  3. Run
  4. Pause
  5. Stop
  6. Auto-Scroll (have this activated in normal use)
  7. Auto-Scale
  8. Customize Graphs

Task 1.2 - Filtered data

A good place to start looking is at the raw TDC data. This is done by expanding the output struct and click the box in front of pTdcValueRaw.

By pressing the capacitive element connected labeled BTN1 on the ULPSENSE boosterpack the pTdcValueRaw should increase. By touching the BTN1 the capacitance on this pin increase. This will increase the time it takes to charge this capacitance up to the reference voltage. The raw value is used to evaluate of the capacitive sensing element has been touched or not.

The raw data can be quite noisy, which the filtering algorithm will try to suppress the best it can.

  • [0] is the true raw value read from the TDC. It's completely unfiltered.
  • Baseline: this is the infinite impulse response of the long term average value of when no capacitive element is touched. This value is a dynamic value.
  • Threshold: this is the value, to which the filtered value is compared, in order to evaluate touch. This is dynamic and is calculated as baseline + 0.5 (highvalue - baseline).
  • High: this is a filtered TDC value when you touch the capacitive touch element.
  • TdcSmplMean: the resulting moving average value.

In order to see the Raw, Baseline, High, Threshold and TdcSmplMean values for BTN2, open the corresponding pTdcValueRaw, pBaselineValue, pHighValue, pThresValue, pTdcSmplWindowMeantab and click the box for [1].

If you can't see any data or your graphs seems noisy:

The Run-Time Logging needs a high UART datarate. Check that the baud rate is set to 230400 in the Run-Time Logging window. Check that the plexiglas is mounted firmly on the PCB and that the boosterpack is fully attached to the launchpad.

Task 1.3 - Customize Graphs

In order to add the corresponding graphs to the already existing view, use the Customize Graphs option:

  • Click Customize Graphs
  • Select which graph you want to add child graphs to in the drop down for Select parent graph, for example output.pTdcValueRaw[1]
  • Press Add child graph
  • Select the child graph you want to add, for example pTdcSmplWindowMean[1]
  • Repeat with graphs of your choice

Here, we have added the corresponding graphs to BTN2 that were already added for BTN1. This is useful when you want to compare different filter in the same window rather than having one window for each filter.

Notice that when the code detects a touch, the sample rate is increased. The variables controlling this are:

  • cfg.maxSamplePeriod for the non-detect sample rate
  • cfg.minSamplePeriod for when the element is touched

This is to save power when no touch is being detected.

Task 1.4 - Detection

For most applications, a simple pressed/not pressed functionality is enough. If the TdcSmplMean value exceeds the Threshold value, a detection is set from 0 to 1. If you check the pTouchDet box a new graph window will open with a binary value representing the touch.

If you want your pTouchDet graph to register faster or slower, this can be done by changing the threshold value. By default it's:

// Set the touch detection threshold to 1/2 between the baseline and the high value
U16 thresValue = baselineValue + ((highValue - baselineValue) >> 1);

For example, change the right shifted >> 1 to something else in order to make the division larger/ smaller.

Great job!

You have now tested a basic capacitive touch algorithm using a SimpleLink device!

Task 2 – Trimming the Capacitive Touch Algorithm

If you're not using the BOOSTXL-ULPSENSE BoosterPack, but rather a custom hardware, you know that different touch elements have different sensitivity. This can cause the graphs to not react at all, or to be triggered at all times. Either way, trimming the current which is charging the capacitance might be able to save the day!

The less current that is charging the capacitor, the more sensitive the input becomes. Different register values will give different currents between 0.25 uA and 16.25 uA. The default value for the current slider is 4, which corresponds to 1 uA.

Everytime you change the value of the slider, you have to restart the run-time logging. Press the tool bar button or use F7.

Nice Job!

When you are satisfied with the current and voltage trimming, move on to Task 3 to see the inner workings of the algorithm.

Task 3 – A Closer Look At The SCS Code (Optional)

We are now ready to take a look at some key implementations in the code. If you at any moment want to read more about an API call, open Sensor Controller Studio > Help > Sensor Controller Studio Help, or press F1. As soon as the Help Viewer is open, you can use the search box to hopefully find the information you are looking for. The SCS help can also be found her

A Sensor Controller project is built up by code called at four different times:

Panel Tab Description
Code that will run once after initialization of task (scifStartTasksNbl()).
The Execution Code will run when scheduled (fwScheduleTask()).
Code that is triggered by a configured event (GPIO, COMPB or timer).
Termination code run once on task exit or never if the task runs forever.

The Execution Code is empty since this project does not make use of any code scheduled by the TI-RTOS, but rather triggered periodically by Timer A.

Initialization Code

We start with initialization of the pins we chose in the I/O mapping.

// Clamp all capactive touch pins to ground
for (U16 n = 0; n < PIN_COUNT; n++) {
    isrcClamp(cfg.pAuxioAxdCapTouch[n]);
}

The reference should not be pulled high at this point in the code.

// Do not drive the reference high
gpioClearOutput(AUXIO_AXS_CAP_TOUCH_REF);

In order to start the periodically called Event Handler A Code to do the actual TDC readings we schedule its first execution by calling:

// Schedule the first execution
evhSetupTimer1Trigger(0, 1, 5);

Looking at the documentation we can see that the parameters are:

  • evIndex - Event index to be triggered
  • mant - Delay mantissa value (1-255). Resulting delay is mant * 2^exp [4 kHz ticks].
  • exp - Delay exponent value (0-15). Resulting delay is mant * 2^exp [4 kHz ticks].

Looking again at this picture we can see that Event Index 0 will trigger our Event Handler A Code. This explains the first parameter 0.

Event Handler A Code

As stated earlier there is no Execution Code, but rather the Event Handler A Code periodical executed based on a timer. This whole block of code will run every time the timer 1 is triggered.

  1. Peripherals are initialized
  2. Reference voltage is chosen
  3. The current charging the capacitance is set
  4. Clock source is chosen
  5. TDC is being read in a loop for each pin
  6. TDC value processing
  7. Output is generated
  8. Sample period is being calculated
  9. Peripherals are being released
  10. The next measurement is scheduled

The code uses three different types of data structures: cfg, state and output.

  • cfg contains the constant variables we initialize. They can be changed depending on your application but will never be updated runtime.
  • state contains variables that are dynamically updated between iterations in order for us to calculate the output variables.
  • output contains the output variables that we can use for different application purposes.

Some of the code is self-explanatory, but the part about the data being filtered can be hard to grasp, which is why we have chosen to talk more about it here.

TDC value processing

The TDC value has to go through a series of post processing steps to detect if a button has been touched.

The two following figures gives a graphical representation of the flow.

The processing steps are named and numbered and the following sections will show the code for the various steps.

Step 1: TDC timeout handling

Handle TDC timeout: Reuse last value until MAX_TDC_TIMEOUTS timeouts in a row, then it's an error. For the very first iteration the timeout count is initialized to MAX_TDC_TIMEOUTS - 1. The variable tdcTimeoutCount is available in the Run-Time Logging view for monitoring.

    // Handle TDC timeout: Reuse last value until MAX_TDC_TIMEOUTS timeouts in a row, then it's an error
    // For the very first iteration the timeout count is initialized to MAX_TDC_TIMEOUTS - 1
    U16 tdcTimeoutCount = state.pTdcTimeoutCount[n];
    if (isTdcDone == 1) {
        tdcTimeoutCount = 0;
    } else {
        tdcValueL = *pTdcSmplWindowPinBase;
        utilIncrAndSat(tdcTimeoutCount, MAX_TDC_TIMEOUTS; tdcTimeoutCount);
        if (tdcTimeoutCount == MAX_TDC_TIMEOUTS) {
            state.bvTdcTimeout |= 1 << n;
        }
    }
    state.pTdcTimeoutCount[n] = tdcTimeoutCount;

Step 2: Hard-limit

Hard-limit so that arithmetic overflow cannot occur. The variable bvTdcValueOverflow is available in the Run-Time Logging view for monitoring.

    // Handle TDC value overflow: Hard-limit so that arithmetic overflow cannot occur
    if (TDC_SMPL_WINDOW_EXP >= LOW_MEAN_WINDOW_EXP) {
        if (tdcValueL >= (0xFFFF >> TDC_SMPL_WINDOW_EXP)) {
            tdcValueL = 0xFFFF >> TDC_SMPL_WINDOW_EXP;
            state.bvTdcValueOverflow |= 1 << n;
        }
    } else {
        if (tdcValueL >= (0xFFFF >> LOW_MEAN_WINDOW_EXP)) {
            tdcValueL = 0xFFFF >> LOW_MEAN_WINDOW_EXP;
            state.bvTdcValueOverflow |= 1 << n;
        }
    }

Step 3: Spike value handling

This step filter away any raw TDC spikes. The value compared to the raw TDC data is calculated every iteration and is dependent on the last iterations baseline- and high value.

    // Handle TDC value spikes: Saturate the TDC value to a range around the current baseline
    if (state.startupIterCount > TDC_SMPL_WINDOW_SIZE) {

        // The TDC value's maximum deviation from the baseline is 1.25 * (filterHighVal - baseline)
        U16 baselineValue = state.pBaselineValue[n];
        U16 highBaselineDiff = state.pHighValue[n] - baselineValue;

        // Ensure that the TDC value is in the range [baseline - margin, baseline + 2 * margin]
        U16 minTdcValueL = baselineValue - highBaselineDiff;
        U16 maxTdcValueL = baselineValue + (highBaselineDiff << 1);
        if (tdcValueL < minTdcValueL) {
            tdcValueL = minTdcValueL;
        } else if (tdcValueL > maxTdcValueL) {
            tdcValueL = maxTdcValueL;
        }
    }

Step 4: pTdcSmplWindow

    // Finalize the TDC value window mean
    tdcSmplWindowMean += tdcValueL;
    tdcSmplWindowMean >>= TDC_SMPL_WINDOW_EXP;
    output.pTdcSmplWindowMean[n] = tdcSmplWindowMean;

Step 5 and 6: pLowMeanWindow and IIR filter

These steps of the code are conditinal and will be performed each time the TDC value window has been replaced while not detecting a touch. First The lowMeanWindowMean is updated before the result is filtered through a IIR filter.

    // Each time the TDC value window has been replaced while not detecting a touch, add the
    // low TDC value window mean to the low mean window
    if (state.pTdcSmplLowCount[n] >= TDC_SMPL_WINDOW_SIZE) {
        ifnot (state.pTdcSmplLowCount[n] & (TDC_SMPL_WINDOW_SIZE - 1)) {

            // Shift the current low mean window, and calculate the mean value of the moved values
            U16* pLowMeanItem = pLowMeanWindowPinBase + (LOW_MEAN_WINDOW_SIZE - 2);
            U16 lowMeanWindowMean = 0;
            while (pLowMeanItem >= pLowMeanWindowPinBase) {
                U16 lowMeanItemValue = *(pLowMeanItem++);
                *pLowMeanItem = lowMeanItemValue;
                pLowMeanItem -= 2;

                // Add to the average, using the
                if (lowMeanItemValue == 0) {
                    lowMeanWindowMean += tdcSmplWindowMean;
                } else {
                    lowMeanWindowMean += lowMeanItemValue;
                }
            }

            // Insert the new TDC value window mean, and finalize the sum
            *pLowMeanWindowPinBase = tdcSmplWindowMean;
            lowMeanWindowMean += tdcSmplWindowMean;
            lowMeanWindowMean >>= LOW_MEAN_WINDOW_EXP;

            // Calculate the low mean value by IIR filtering
            if (state.startupIterCount < LOW_MEAN_IIR_UPDATE_PERIOD) {
                state.pLowMeanIirFilter[n] = lowMeanWindowMean;
            } else {
                iirFilter(state.pLowMeanIirFilter[n], lowMeanWindowMean, LOW_MEAN_IIR_DIV_EXP);
            }
        }
    }

Step 7/ step 8: Update pBaselineValue/ IIR filter

Dependent on the state of pTouchDet either step 7 (the if statement return true) or step 8 (the if staement return false) will be done.

if (state.pTouchDet[n] == 0) {

            // Update the baseline after a backoff period
            U16 lowMeanIirFilter = state.pLowMeanIirFilter[n];
            if (state.pTdcSmplLowCount[n] > (LOW_MEAN_IIR_UPDATE_PERIOD + BASELINE_UPDATE_PERIOD)) {

                // The baseline value tracks faster when going down than when going up
                if (lowMeanIirFilter < baselineValue) {
                    iirFilter(baselineValue, lowMeanIirFilter, 1);
                } else {
                    iirFilter(baselineValue, lowMeanIirFilter, 3);
                }
                state.pBaselineValue[n] = baselineValue;

                // Back off
                state.pTdcSmplLowCount[n] -= BASELINE_UPDATE_PERIOD;
            }

        // Otherwise ...
        } else {

            // Update the high value
            iirFilter(highValue, tdcSmplWindowMean, 4);
        }

Step 9: Update threashold value

output.pBaselineValue[n] = baselineValue;

        // Limit the high value to the range [baseline + 1/32, baseline + 1/4]
        U16 minHighValue = baselineValue + (baselineValue >> 5);
        if (highValue < minHighValue) {
            highValue = minHighValue;
        }
        U16 maxHighValue = baselineValue + (baselineValue >> 2);
        if (highValue > maxHighValue) {
            highValue = maxHighValue;
        }
        state.pHighValue[n] = highValue;
        output.pHighValue[n] = highValue;

        // Set the touch detection threshold to 1/2 between the baseline and the high value
        U16 thresValue = baselineValue + ((highValue - baselineValue) >> 1);
        output.pThresValue[n] = thresValue;

Step 10: Detect touch

Determine if a touch is detected or not by comparing with the threshold value. The sample time is also adjusted in this step.

// Detect touch
        U16 touchDetNow;
        if (tdcSmplWindowMean > thresValue){
            touchDetNow = 1;
            state.pTdcSmplLowCount[n] = 0;
            state.smplPeriod = cfg.minSmplPeriod;
        } else {
            touchDetNow = 0;
            state.pTdcSmplLowCount[n] += 1;
        }

        // Find whether we should generate any output
        if (touchDetNow != state.pTouchDet[n]) {
            state.pTouchDet[n] = touchDetNow;
            state.genOutput = 1;
        } else {
            state.genOutput |= touchDetNow;
        }
        output.pTouchDet[n] = touchDetNow;

Remaining code

The code not covered in the above is typically for initialization or error handling.

Schedule next iteration

At the bottom of the code you can see the same call as in the bottom of the initialization code.

// Schedule the next execution
evhSetupTimer1Trigger(0, state.smplPeriod, 0);

This line has to be called every time after each execution in order to schedule for the next period. Here we can see that the state.smplPeriod is used in order to dynamically set the sample time.

Termination Code

The Termination Code must call evhCancelTrigger() for each used event index. In our case that is once again Event Index 0.

// Stop triggering the event handler code
evhCancelTrigger(0);

Job Well Done!

You should now have gained some insight in to how capacitive sensing works using the SCS Run-Time Logging tool.

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.