Capacitive Touch on the nRF52 series

Important updates:

Introduction

Scope

The following topics will be covered in a varying degree of detail:

  • Use cases for capacitive sensing.
  • Basic principles of capacitive sensing.
  • A simple example of capacitive buttons on the nRF52.

The details of analyzing capacitive sensors is out of scope of this tutorial. As is advanced processing of the sensor data. The accompanying example is simple, but is provided as a proof of concept and a possible starting point for further development.

Required prior knowledge

It is expected that you are familiar with the basics of embedded C programming. You should also have basic knowledge of how to build and download firmware to the nRF52 DK. Refer to Getting Started in the SDK documentation if this is unfamiliar territory. Moreover, the example use a few nordic libraries that will not be explained in detail. Refer to the SDK documentation for details on those.

Required equipment and software

Hardware:

  • nRF52 DK.
  • Capacitive sensor of some sort (a piece of copper tape will do).

Software:

Other files:

Use cases for capacitive sense on the nRF52

Capacitive sensors have no moving parts and are normally protected by a surface material, such as glass or plastic. This makes capacitive sensors more reliable than mechanical sensors, and a better choice for products that are exposed to the environment or need regular cleaning. The sensor can be made in any shape, and need not be on a flat surface. Moreover, with the built in support in the nRF52, capacitive sensors are very low cost, as no external components are required.

The sensor can be made to have a application range, from fractions of a millimeter up to several centimeters. Depending on the sensor it can be used to make precise buttons, or it can be used to detect if a device is held in a hand or not.

The following is a list of possible use cases for capacitive sensing with the nRF52:

  • Buttons
  • Sliders
  • Rotation sliders
  • Detect if held by hand or not

Principles of capacitive sensing

Capacitive sensing is based on capacitive coupling and utilizes the body capacitance. Refering to the parallel-plate model of a capacitor, the sensor electrode (usually copper on the PCB)) can be seen as a plate of a capacitor, where for example air, glass or plastic is the dielectric and the finger acts as the other plate.

The capacitance, C = (εA)/d, where ε is the permittivity of the dielectric material between the plates, A is the area of the plates, and d is the distance between the plates. A large object such as a hand will result in a higher capacitance than a small object such as a finger. Moreover, a close object will result in a higher capacitance than a remote object. This means a close finger may result in the same capacitance as a more remote hand, making the two cases indistinguishable.

Changes in capacitance can be monitored in order to detect changes in the environment, such as a finger closing in on a button. In a nutshell, the higher capacitance, the "harder" the press.

The design of a sensor depends on the use case. As the size of the sensor electrode determines the sensitivity of the sensor, a generic proximity sensing sensor would normally have a large surface, as shown in the picture.

image description

Button type sensors will normally be smaller, as is the case with this 8 button keypad.

image description

Simple sliders or rotation sliders can be made from separate buttons in close proximity. However, using more complex sensor shapes make it possible to get higher resolution using fewer pins. With a sensor configuration similar to that shown below, the finger will always interact with two sensors simultaneously, and the location of the finger on the slider can be quite accurately calculated. For a slider this would require at least two electrodes, and for a rotation switch a minimum of three electrodes must be used. The sketch below shows a slider with 4 electrodes.

image description

The capacitive sensor electrode is often just copper on the PCB.

There are several methods that can be used to detect changes in capacitance. This tutorial will only mention two, which use different methods of detecting the changes in capacitace based on changes in the rise and fall time (given by the RC time constant):

  1. Resistor capacitor (RC) charge timing.
  2. Relaxation oscillator (RO).

RC charge timing

Using RC charge timing, the change in capacitance is found by measuring the charging or discharging time of the sensor capacitor via a resistor. Thus it requires one external component, a resistor. Two pins are required per button:

  • one GPIO output pin for charging the sensor capacitor via a large resistor (e.g. 1–10 MΩ).
  • one analog input pin connected to the sensor electrode at the same point as the GPIO (without the resistor). This is used to measure the voltage over the sensor capacitor.

The measuring device (e.g. a nRF5x) will measure the time it takes to charge the sensor capacitor via the series resistor. This measurement can be considered a sample, and sampling will be performed at regular intervals.

The nrf51-capsense-example is a example of this method. It uses two pins and relies on the low power comparator (LPCOMP) and timer (TIMER) peripherals. The example is also valid on the nRF52 with minor modifications.

Relaxation oscillator

The relaxation oscillator method builds an oscillator which uses the sensor capacitor as a timing element. The frequency of the oscillator will dependent on the rise and fall time of the sensor capacitance. As the capacitance increases when an object is close to the sensor, the time constant will be larger and the frequency lower.

image description

The comparator (COMP) module in the nRF52 includes hardware support for capacitive sensing. It can be configured to enable a current source that outputs a current on the selected analog pin, and create a feedback path around the comparator, forming a relaxation oscillator. A timer can be used to measure the period of the oscillator. These measurements can be done in hardware, by connecting comparator events to timer tasks using Programmable Peripheral Interconnect (PPI).

Calibration and post processing of samples

Both the methods described above will result in samples that in one way or the other is related to the capacitance of the sensor capacitor. These raw samples need processing, and cannot be used directly to indicate button presses. Both calibration and debouncing is needed.

Calibration of capacitive sensors is not straightforward. The sensor capacitance will change significantly not only due to the presence of objects (such as a human finger), but also due to changes in humidity, overlay materials (e.g. screen protector) etc. There can also be significant differences depending on how the device is used, such as if it is held in a hand or lying on a desk. Moreover, the nRF52 peripherals will perform differently at different temperatures, supply voltages etc. A proper calibration mechanism must be able to properly handle these changes in the environment seamlessly, while not mistaking a long touch as a change in the environment.

Example: Capacitive button sensing on the nRF52

This example will demonstrate how the Comparator peripheral (COMP) in the nRF52 can be used to implement capacitive buttons using only 1 pin per sensor and no external components. Two capacitive buttons are used; each will toggle a LED. The LED will glow when the button is "pressed". Though the nRF52 can also be used with complex sliders, as described previously in this tutorial, the example code can only handle simple buttons.

Hardware

The example requires a nRF52 DK and some form of capacitive sensor. The capacitive sensor can be very simple, typically just a conductive surface. Virtually any form of capacitive sensor can be used. The following sensor is more than adequate, and can be made in less than five minutes:

  1. Cut a piece of cardboard to whatever size and shape you want for your button.
  2. Put copper tape on one surface.
  3. Solder a wire on the copper surface.

The sensor can look something like this:

image description

Connect the sensor to one of the 8 analog input pins on the nRF52 DK. The mapping between the analog input numbers and GPIO port numbers (printed on the PCB silkscreen) can be found in the Pin assignments section in the Objective Product Specification.

The rest of the example will assume that two sensors are used, and that they are connected to analog input 2 and 3, marked as P0.04 and P0.05 on the PCB of the nRF52 DK. They each represent a button. The setup is shown below.

image description

Software

The nrf52-capsense-example example project has a simple capasitive sense driver and example. The example in its current state is intended as a proof of concept, and is not suitable for end products.

The example is based on nRF5 SDK 11. It is intended for the zipped version of the SDK and will not work with the Pack installer.

To get started, clone the nrf52-capsense-example repository to <SDK>\examples\peripheral. The project comes with a Makefile for GCC (pca10040\blank\armgcc\Makefile) and a project for Keil 5 (pca10040\blank\arm5_no_packs\nrf52_capsense_example_blank_pca10040.uvprojx).

The project uses the following hardware resources on the nRF52:

  • The comparator.
  • One of the timers (TIMER1 by default).
  • 2 PPI channels.
  • A real time counter (RTC1, using the application timer library).

Most parameters of the driver are configured by modifying the defines in nrf_capsense_cfg.h. This include number of buttons, calibration, debouncing and allocation of timer and PPI channel resources. The nrf_capsense_init() function is used to initialize the driver by supplying a pointer to a static configuration struct. This struct, nrf_capsense_cfg_t, holds a function pointer to the application's event handler (callback function) and the sensor pin numbers.

The example measures the time of a half period of the signal generated by the relaxation oscillator, which frequency depend on the sensor capacitance. Sampling is initiated by software, but the complete sampling sequence is performed in hardware using PPI. Once a sample is collected it is post-processed in software, applying calibration and debouncing.

A raw sample is generated in the following way:

  1. Sampling is initiated by setting the pin select register in the comparator to the pin of the button to be sampled. then starting the comparator. This effectively starts the oscillator.
  2. The first upward crossing of the comparator starts the timer via PPI.
  3. The downwards crossing of the comparator causes the timer to capture the timer value and stops the timer (again via PPI). It also triggers a comparator interrupt.
  4. The comparator interrupt handler reads the raw sample value, which is stored in the timers capture register. This is the time between a upward crossing and a downward crossing, which is effectively the time of a half period.

Before the buttons can be used, they must be calibrated. In this simple example, calibration data is only collected at startup by calling nrf_capsense_calibrate(). This must be done after the call to nrf_capsense_init() and before any calls to nrf_capsense_sample(). Those calibration data never change. Note that this is based on the naive assumption that buttons are never pressed when calibration is run and, even more important, that the environment never changes. This assumption may hold at a engineers desk, but will not hold in a end product that is used in a variety of ways in a variety or changing environments. The calibration process is done by simply collecting a number of raw samples, storing the min and max value, and using (min + max) / 2 as the base line.

During normal operation, raw samples are collected as described previously. Then each raw sample is compared with calibration data, and a decision is made. If a sample is higher than the baseline + calibration margin, it is decided that the button is likely being pressed. At this point, the raw data is similar to that of a physical push button, and the remaining step is debouncing. The debouncing requires several processed samples to have the same value. If the state of a button has changed after debouncing, the callback function is called, informing the application.

The example application calls nrf_capsense_sample() every 10 milliseconds. When a change in a button is detected, the LED's are updated.

Building and running the example

If the example is extracted to a valid location in the SDK (e.g. examples/periperal), it should build without the need of any modification. Make sure that sensors are connected to analog input pin 2 and 3, or modify the pin numbers in the init_capsense() function in main.c accordingly.

The example use the UART/RTT logging library. By default the project is configured to log via RTT, as NRF_LOG_USES_RTT=1 is defined in the Makefile and Keil project. Open the SEGGER J-Link RTT Viewer and connect to the onboard debugger in order to view the log messages.

Once sensors are connected and application flashed, touching a sensor will cause the corresponding LED to light up. Moreover, the button mask is logged at every change.

Visualisation of the relaxation oscillator frequency

The frequency of the RO can be visualized by connecting the comparator to a GPIO output using PPI and GPIOTE. Note that the example disabled the comparator peripheral between samples, so you would not see a continuous signal unless the example is modified to use a single sensor and to leave the comparator peripheral always running.

In order to output the RO signal, configure a GPIOTE output to toggle on task out events, and connect the GPIOTE TASKS_OUT endpoint to the comparator EVENTS_CROSS. By connecting the GPIO to an oscilloscope one can easily see the effects of manipulating the sensor capacitance by e.g. moving a hand over the sensor.

Further improvements

Particularly the calibration method need more work before the example can be used in an end product. The calibration algorithm must seamlessly handle changes in the environment, as briefly outlined earlier in this tutorial.

In order to obtain a better compromise between power consumption and user experience it makes sense to use a slow sampling rate when the user is not interacting with the device. Once user interaction has been detected, the sampling rate could be increased and held high until a certain time after the last button press.

  • Unknown
    Unknown in reply to Tim Kutscha

    You’ve got some interesting points in this article. I would have never considered any of these if I didn’t come across this. Thanks!. Suika Game

  • Thank you very much. This information is really useful for me and I want to tell you that the article is great Rainbow Friends: Chapter 2 

  • Hello

    I think the "Important updates" on top of this article sends a mixed message.

    - The first bullet says that we should not use the COMP/Isource/relaxation oscillator method due to error in the Isource / comp.

    - The second bullet says that we should use the new capacitive sensor driver and capacitive sensor library.

    However, according to the errata for nrf52832 Rev 2, it looks like the Isource / comp issue is still active. At the same time, the new capacitive sensor driver looks like it is relying on COMP/Isource/relaxation.

    We want to equip an nRF52832 with a capacitive sensors (that also should work outside of room temperature). Is the correct way forward to ignore the new capacitive sensor driver and instead do a custom RC charge time implementation?

  • Here's the source file from the last comment.

    /**
     * Copyright (c) 2016 - 2019, Nordic Semiconductor ASA
     *
     * All rights reserved.
     *
     * Redistribution and use in source and binary forms, with or without modification,
     * are permitted provided that the following conditions are met:
     *
     * 1. Redistributions of source code must retain the above copyright notice, this
     *    list of conditions and the following disclaimer.
     *
     * 2. Redistributions in binary form, except as embedded into a Nordic
     *    Semiconductor ASA integrated circuit in a product or a software update for
     *    such product, must reproduce the above copyright notice, this list of
     *    conditions and the following disclaimer in the documentation and/or other
     *    materials provided with the distribution.
     *
     * 3. Neither the name of Nordic Semiconductor ASA nor the names of its
     *    contributors may be used to endorse or promote products derived from this
     *    software without specific prior written permission.
     *
     * 4. This software, with or without modification, must only be used with a
     *    Nordic Semiconductor ASA integrated circuit.
     *
     * 5. Any software provided in binary form under this license must not be reverse
     *    engineered, decompiled, modified and/or disassembled.
     *
     * THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS
     * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
     * OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE
     * DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE
     * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
     * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
     * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     *
     */
    
    // When we update the SDK, we'll need to add these changes to
    // get the cap sensor code working since the embedded current
    // source is broken.  TODO: trim down this complex code and
    // put it in the main code branch so we don't have to update
    // vendor-supplied files
    #define SIMPLEXITY_CHANGES
    
    #include "sdk_common.h"
    #if NRF_MODULE_ENABLED(NRF_DRV_CSENSE)
    #include "nrf_drv_csense.h"
    #include "nrf_peripherals.h"
    #include "nrf_gpio.h"
    #include "app_error.h"
    #include "app_util_platform.h"
    #include "nrf_assert.h"
    #include "string.h"
    #include <stdio.h>
    
    #if defined(__CORTEX_M) && (__CORTEX_M < 4)
    #ifndef ARM_MATH_CM0PLUS
    #define ARM_MATH_CM0PLUS
    #endif
    /*lint -save -e689 */
    #include "arm_math.h"
    /*lint -restore */
    #endif
    
    #if USE_COMP
    #include "nrf_drv_comp.h"
    #include "nrf_drv_ppi.h"
    #include "nrf_drv_timer.h"
    #ifdef SIMPLEXITY_CHANGES
    #include "nrfx_gpiote.h"
    #endif
    #endif //USE_COMP
    
    #if USE_COMP == 0
    #ifdef ADC_PRESENT
    #include "nrfx_adc.h"
    
    /**
     * @defgroup adc_defines ADC defines to count input voltage.
     * @{
     */
    #define ADC_RES_10BIT           1024
    #define ADC_INPUT_PRESCALER     3
    #define ADC_REF_VBG_VOLTAGE     1.2
    /* @} */
    
    /* ADC channel used to call conversion. */
    static nrfx_adc_channel_t adc_channel = NRFX_ADC_DEFAULT_CHANNEL(NRF_ADC_CONFIG_INPUT_0);
    #elif defined(SAADC_PRESENT)
    #include "nrf_drv_saadc.h"
    
    /**
     * @defgroup saadc_defines SAADC defines to count input voltage.
     * @{
     */
    #define SAADC_RES_10BIT           1024
    #define SAADC_INPUT_PRESCALER     3
    #define SAADC_REF_VBG_VOLTAGE     0.6
    /* @} */
    
    /* SAADC channel used to call conversion. */
    static nrf_saadc_channel_config_t saadc_channel = NRF_DRV_SAADC_DEFAULT_CHANNEL_CONFIG_SE(NRF_SAADC_INPUT_AIN0);
    #endif //ADC_PRESENT
    #endif //USE_COMP
    
    #if USE_COMP
    /* Number of channels required by PPI. */
    #ifdef SIMPLEXITY_CHANGES
    #define PPI_REQUIRED_CHANNELS 5
    #else
    #define PPI_REQUIRED_CHANNELS 3
    #endif
    
    /* Array of PPI channels. */
    static nrf_ppi_channel_t m_ppi_channels[PPI_REQUIRED_CHANNELS];
    
    
    /**
     * @defgroup timer_instances Timer instances.
     * @{
     */
    static const nrf_drv_timer_t m_timer0 = NRF_DRV_TIMER_INSTANCE(TIMER0_FOR_CSENSE);
    static const nrf_drv_timer_t m_timer1 = NRF_DRV_TIMER_INSTANCE(TIMER1_FOR_CSENSE);
    #ifdef SIMPLEXITY_CHANGES
    static nrfx_gpiote_out_config_t m_gpiote = NRFX_GPIOTE_CONFIG_OUT_TASK_HIGH;
    #endif
    /* @} */
    #endif //USE_COMP
    
    /* Configuration of the capacitive sensor module. */
    typedef struct
    {
        volatile nrfx_drv_state_t       module_state;                       /**< State of the module. */
        nrf_drv_csense_event_handler_t  event_handler;                      /**< Event handler for capacitor sensor events. */
        uint16_t                        analog_values[MAX_ANALOG_INPUTS];   /**< Array containing analog values measured on the corresponding COMP/ADC channel. */
        volatile bool                   busy;                               /**< Indicates state of module - busy if there are ongoing conversions. */
        volatile uint8_t                cur_chann_idx;                      /**< Current channel to be read if enabled. */
        volatile uint8_t                adc_channels_input_mask;            /**< Enabled channels. */
        uint8_t                         output_pin;                         /**< Pin to generate signal charging capacitors. */
        uint8_t                         channels_to_read;                   /**< Mask of channels remaining to be read in the current measurement. */
        volatile bool                   timers_powered_on;                  /**< Flag to indicate if timers were already started. */
    }csense_t;
    
    static csense_t m_csense;
    
    /**
     * @brief Function for determining the next analog channel to be read.
     */
    __STATIC_INLINE void calculate_next_channel(void)
    {
        m_csense.cur_chann_idx = 31 - __CLZ(m_csense.channels_to_read);
    }
    
    /**
     * @brief Function for handling conversion values.
     *
     * @param[in] val                Value received from ADC or COMP.
     */
    static void conversion_handler(uint16_t val)
    {
        nrf_drv_csense_evt_t event_struct;
    
    #if USE_COMP == 0
        nrf_gpio_pin_set(m_csense.output_pin);
    #endif //USE_COMP
    
        m_csense.analog_values[m_csense.cur_chann_idx] = val;
    
        event_struct.read_value = val;
        event_struct.analog_channel = m_csense.cur_chann_idx;
    
        m_csense.channels_to_read &= ~(1UL<<m_csense.cur_chann_idx);
    
        // decide if there will be more conversions
        if (m_csense.channels_to_read == 0)
        {
            m_csense.busy = false;
    #if USE_COMP == 0 && defined(SAADC_PRESENT)
            nrf_saadc_disable();
    #endif
        }
    
        m_csense.event_handler(&event_struct);
    
        if (m_csense.channels_to_read > 0)     // Start new conversion.
        {
            ret_code_t err_code;
            calculate_next_channel();
            err_code = nrf_drv_csense_sample();
            if (err_code != NRF_SUCCESS)
            {
                return;
            }
        }
    }
    
    #if USE_COMP
    /**
     * @brief Timer0 interrupt handler.
     *
     * @param[in] event_type Timer event.
     * @param[in] p_context  General purpose parameter set during initialization of
     *                       the timer. This parameter can be used to pass
     *                       additional information to the handler function, for
     *                       example, the timer ID.
     */
    static void counter_compare_handler(nrf_timer_event_t event_type, void* p_context)
    {
        if (event_type == NRF_TIMER_EVENT_COMPARE0)
        {
            uint16_t val =  nrf_drv_timer_capture_get(&m_timer1, NRF_TIMER_CC_CHANNEL1);
            nrf_drv_timer_pause(&m_timer1);
            nrf_drv_timer_clear(&m_timer1);
    
            /* Handle finished measurement. */
            conversion_handler(val);
        }
    }
    
    /**
     * @brief Dummy handler.
     *
     * @param[in] event_type Timer event.
     * @param[in] p_context  General purpose parameter set during initialization of
     *                       the timer. This parameter can be used to pass
     *                       additional information to the handler function, for
     *                       example, the timer ID.
     */
    static void dummy_handler(nrf_timer_event_t event_type, void* p_context){}
    
    /**
     * @brief Function for initializing timers.
     *
     * @retval NRF_ERROR_INTERNAL            If there were error initializing timers.
     * @retval NRF_SUCCESS                   If timers were initialized successfully.
     */
    static ret_code_t timer_init(void)
    {
        ret_code_t err_code;
    
        //set first timer in timer mode to get period of relaxation oscillator
        nrf_drv_timer_config_t timer_config = NRF_DRV_TIMER_DEFAULT_CONFIG;
        timer_config.mode = NRF_TIMER_MODE_TIMER;
        err_code = nrf_drv_timer_init(&m_timer1, &timer_config, dummy_handler);
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
    
        //set second timer in counter mode and generate event on tenth period
        timer_config.mode = NRF_TIMER_MODE_COUNTER;
        err_code = nrf_drv_timer_init(&m_timer0, &timer_config, counter_compare_handler);
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
        nrf_drv_timer_extended_compare(&m_timer0, NRF_TIMER_CC_CHANNEL0, MEASUREMENT_PERIOD, (nrf_timer_short_mask_t)(NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK | NRF_TIMER_SHORT_COMPARE0_STOP_MASK), true);
    
        return NRF_SUCCESS;
    }
    
    /**
     * @brief Function for initializing and enabling PPI channels.
     *
     * @retval NRF_ERROR_INTERNAL            If there were error initializing or enabling PPI channels.
     * @retval NRF_SUCCESS                   If PPI channels were initialized and enabled successfully.
     */
    static ret_code_t ppi_init(void)
    {
        ret_code_t err_code;
        uint8_t i;
    
        err_code = nrf_drv_ppi_init();
        if ((err_code != NRF_SUCCESS) && (err_code != NRF_ERROR_MODULE_ALREADY_INITIALIZED))
        {
            return NRF_ERROR_INTERNAL;
        }
    
        for (i = 0; i < PPI_REQUIRED_CHANNELS ; i++)
        {
            err_code = nrf_drv_ppi_channel_alloc(&m_ppi_channels[i]);
            if (NRF_SUCCESS != err_code)
            {
                return NRF_ERROR_INTERNAL;
            }
        }
    
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channels[0], nrf_drv_comp_event_address_get(NRF_COMP_EVENT_CROSS), nrf_drv_timer_task_address_get(&m_timer0, NRF_TIMER_TASK_COUNT));
        if (NRF_SUCCESS != err_code)
        {
           return NRF_ERROR_INTERNAL;
        }
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channels[1], nrf_drv_timer_event_address_get(&m_timer0, NRF_TIMER_EVENT_COMPARE0), nrf_drv_timer_task_address_get(&m_timer1, NRF_TIMER_TASK_CAPTURE1));
        if (NRF_SUCCESS != err_code)
        {
           return NRF_ERROR_INTERNAL;
        }
        err_code = nrf_drv_ppi_channel_fork_assign(m_ppi_channels[1], nrf_drv_comp_task_address_get(NRF_COMP_TASK_STOP));
        if (NRF_SUCCESS != err_code)
        {
           return NRF_ERROR_INTERNAL;
        }
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channels[2], nrf_drv_comp_event_address_get(NRF_COMP_EVENT_READY), nrf_drv_timer_task_address_get(&m_timer0, NRF_TIMER_TASK_CLEAR));
        if (NRF_SUCCESS != err_code)
        {
           return NRF_ERROR_INTERNAL;
        }
        err_code = nrf_drv_ppi_channel_fork_assign(m_ppi_channels[2], nrf_drv_timer_task_address_get(&m_timer1, NRF_TIMER_TASK_CLEAR));
        if (NRF_SUCCESS != err_code)
        {
           return NRF_ERROR_INTERNAL;
        }
    
    #ifdef SIMPLEXITY_CHANGES
        // Simplexity added: To compensate for the failing current source,
        // we use the UP and DOWN events from the COMP block to reset/set a GPIO
        // in the GPIOTE block
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channels[3], nrf_drv_comp_event_address_get(NRF_COMP_EVENT_UP), nrfx_gpiote_clr_task_addr_get(NRF_CSENSE_OUTPUT_PIN));
        if (NRF_SUCCESS != err_code)
        {
           return NRF_ERROR_INTERNAL;
        }
    
        err_code = nrf_drv_ppi_channel_assign(m_ppi_channels[4], nrf_drv_comp_event_address_get(NRF_COMP_EVENT_DOWN), nrfx_gpiote_set_task_addr_get(NRF_CSENSE_OUTPUT_PIN));
        if (NRF_SUCCESS != err_code)
        {
           return NRF_ERROR_INTERNAL;
        }
    #endif
    
        for (i = 0; i < PPI_REQUIRED_CHANNELS ; i++)
        {
            err_code = nrf_drv_ppi_channel_enable(m_ppi_channels[i]);
            if (NRF_SUCCESS != err_code)
            {
                return NRF_ERROR_INTERNAL;
            }
        }
    
        return NRF_SUCCESS;
    }
    
    /**
     * @brief Dummy handler for COMP events.
     *
     * @param[in] event         COMP event.
     */
    static void comp_event_handler(nrf_comp_event_t event){}
    
    /**
     * @brief Function for initializing COMP module in relaxation oscillator mode.
     *
     * @note The frequency of the oscillator depends on threshold voltages, current source and capacitance of pad and can be calculated as f_OSC = I_SOURCE / (2C·(VUP-VDOWN) ).
     *
     * @retval NRF_ERROR_INTERNAL                If there were error while initializing COMP driver.
     * @retval NRF_SUCCESS                       If the COMP driver initialization was successful.
     */
    static ret_code_t comp_init(void)
    {
        ret_code_t err_code;
        nrf_drv_comp_config_t m_comp_config = NRF_DRV_COMP_DEFAULT_CONFIG(NRF_COMP_INPUT_0);
    
        /* Workaround for Errata 12 "COMP: Reference ladder is not correctly calibrated" found at the Errata document
           for your device located at https://infocenter.nordicsemi.com/ */
        *(volatile uint32_t *)0x40013540 = (*(volatile uint32_t *)0x10000324 & 0x00001F00) >> 8;
    
    #ifdef SIMPLEXITY_CHANGES
        // since the current source isn't working, we use thresholds relative to external reference
        // This is simply 1/3 and 2/3 of VDD respectively
        m_comp_config.threshold.th_down = NRFX_VOLTAGE_THRESHOLD_TO_INT(1.0,3.0);
        m_comp_config.threshold.th_up   = NRFX_VOLTAGE_THRESHOLD_TO_INT(2.0,3.0);
    #else
        m_comp_config.isource = NRF_COMP_ISOURCE_Ien10uA;
    #endif
    
        err_code = nrf_drv_comp_init(&m_comp_config, comp_event_handler);
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
    
        return NRF_SUCCESS;
    }
    
    #ifdef SIMPLEXITY_CHANGES
    /**
     * @brief Function for initializing GPIOTE module in relaxation oscillator mode.
     *
     *
     * @retval NRF_ERROR_INTERNAL                If there were error while initializing COMP driver.
     * @retval NRF_SUCCESS                       If the COMP driver initialization was successful.
     */
    static ret_code_t gpiote_init(void)
    {
        ret_code_t err_code;
    
        // initialize basic block
        err_code = nrfx_gpiote_init();
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
        
        // configure task for low to high if OUT[] called, SET[] and CLR[] work as usual
        err_code = nrfx_gpiote_out_init(NRF_CSENSE_OUTPUT_PIN, &m_gpiote);
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
    
        nrfx_gpiote_out_task_enable(NRF_CSENSE_OUTPUT_PIN);
    
        return NRF_SUCCESS;
    }
    #endif //SIMPLEXITY_CHANGES
    #endif //USE_COMP
    
    #if USE_COMP == 0
    #ifdef ADC_PRESENT
    /**
     * @brief ADC handler.
     *
     * @param[in] p_event                Pointer to analog-to-digital converter driver event.
     */
    void adc_handler(nrfx_adc_evt_t const * p_event)
    {
        nrf_gpio_pin_set(m_csense.output_pin);
        uint16_t val;
        val = (uint16_t)(p_event->data.sample.sample *
                         ADC_REF_VBG_VOLTAGE * 1000 *
                         ADC_INPUT_PRESCALER / ADC_RES_10BIT);
        conversion_handler(val);
    }
    
    /**
     * @brief Function for initializing ADC.
     */
    static ret_code_t adc_init(void)
    {
        ret_code_t err_code;
    
        adc_channel.config.config.input = NRF_ADC_CONFIG_SCALING_INPUT_ONE_THIRD;
    
        nrfx_adc_config_t const adc_config = NRFX_ADC_DEFAULT_CONFIG;
        err_code = nrfx_adc_init(&adc_config, adc_handler);
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
    
        nrf_gpio_pin_set(m_csense.output_pin);
    
        return NRF_SUCCESS;
    }
    #elif defined(SAADC_PRESENT)
    /**
     * @brief SAADC handler.
     *
     * @param[in] p_event                Pointer to analog-to-digital converter driver event.
     */
    void saadc_handler(nrf_drv_saadc_evt_t const * p_event)
    {
        nrf_gpio_pin_set(m_csense.output_pin);
        uint16_t val;
        (void)nrf_drv_saadc_buffer_convert(p_event->data.done.p_buffer, 1);
        val = (uint16_t)(*p_event->data.done.p_buffer *
                          SAADC_REF_VBG_VOLTAGE * 1000 *
                          SAADC_INPUT_PRESCALER / SAADC_RES_10BIT);
        conversion_handler(val);
    }
    
    /**
     * @brief Function for initializing SAADC.
     */
    static ret_code_t saadc_init(void)
    {
        ret_code_t err_code;
        static nrf_saadc_value_t saadc_value;
    
        saadc_channel.gain = NRF_SAADC_GAIN1_3;
    
       err_code = nrf_drv_saadc_init(NULL, saadc_handler);
       if (err_code != NRF_SUCCESS)
       {
           return NRF_ERROR_INTERNAL;
       }
    
        nrf_gpio_pin_set(m_csense.output_pin);
    
        err_code = nrf_drv_saadc_channel_init(0, &saadc_channel);
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
    
        err_code = nrf_drv_saadc_buffer_convert(&saadc_value, 1);
        if (err_code != NRF_SUCCESS)
        {
            return NRF_ERROR_INTERNAL;
        }
    
        nrf_saadc_disable();
    
        return NRF_SUCCESS;
    }
    #endif //ADC_PRESENT
    #endif //USE_COMP
    
    ret_code_t nrf_drv_csense_init(nrf_drv_csense_config_t const * p_config, nrf_drv_csense_event_handler_t event_handler)
    {
        ASSERT(m_csense.module_state == NRFX_DRV_STATE_UNINITIALIZED);
        ASSERT(p_config->output_pin <= NUMBER_OF_PINS);
    
        ret_code_t err_code;
    
        if (p_config == NULL)
        {
            return NRF_ERROR_INVALID_PARAM;
        }
    
        if (event_handler == NULL)
        {
            return NRF_ERROR_INVALID_PARAM;
        }
    
        m_csense.busy = false;
    
    #if USE_COMP == 0
        m_csense.output_pin = p_config->output_pin;
        nrf_gpio_cfg_output(m_csense.output_pin);
        nrf_gpio_pin_set(m_csense.output_pin);
    #endif //COMP_PRESENT
    
        m_csense.event_handler = event_handler;
    
    #if USE_COMP
    
    #ifdef SIMPLEXITY_CHANGES
        err_code = gpiote_init();
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    #endif
    
        err_code = comp_init();
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
        err_code = timer_init();
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
        err_code = ppi_init();
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    #else
    #ifdef ADC_PRESENT
        err_code = adc_init();
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    #elif defined(SAADC_PRESENT)
        err_code = saadc_init();
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    #endif //ADC_PRESENT
    #endif //USE_COMP
    
        m_csense.module_state = NRFX_DRV_STATE_INITIALIZED;
    
        return NRF_SUCCESS;
    }
    
    ret_code_t nrf_drv_csense_uninit(void)
    {
        ASSERT(m_csense.module_state != NRFX_DRV_STATE_UNINITIALIZED);
    
        nrf_drv_csense_channels_disable(0xFF);
    
    #if USE_COMP
        ret_code_t err_code;
        uint8_t i;
    
        nrf_drv_timer_uninit(&m_timer0);
        nrf_drv_timer_uninit(&m_timer1);
        nrf_drv_comp_uninit();
        for (i =0; i < 3; i++)
        {
            err_code = nrf_drv_ppi_channel_free(m_ppi_channels[i]);
            if (err_code != NRF_SUCCESS)
            {
                return err_code;
            }
        }
        err_code = nrf_drv_ppi_uninit();
        if (err_code != NRF_SUCCESS)
        {
            return err_code;
        }
    #else
    #ifdef ADC_PRESENT
        nrfx_adc_uninit();
    #elif defined(SAADC_PRESENT)
        nrf_drv_saadc_uninit();
    #endif //ADC_PRESENT
    #endif //USE_COMP
    
        m_csense.module_state = NRFX_DRV_STATE_UNINITIALIZED;
    
        memset((void*)&m_csense, 0, sizeof(m_csense));
    
        return NRF_SUCCESS;
    }
    
    void nrf_drv_csense_channels_enable(uint8_t channels_mask)
    {
        ASSERT(m_csense.module_state != NRFX_DRV_STATE_UNINITIALIZED);
    
        m_csense.busy = true;
    
        m_csense.module_state = NRFX_DRV_STATE_POWERED_ON;
    
        m_csense.adc_channels_input_mask |= channels_mask;
    
        m_csense.busy = false;
    }
    
    void nrf_drv_csense_channels_disable(uint8_t channels_mask)
    {
        ASSERT(m_csense.module_state == NRFX_DRV_STATE_POWERED_ON);
    
        m_csense.adc_channels_input_mask &= ~channels_mask;
    
        if (m_csense.adc_channels_input_mask == 0)
        {
            m_csense.module_state = NRFX_DRV_STATE_INITIALIZED;
        }
    }
    
    uint16_t nrf_drv_csense_channel_read(uint8_t csense_channel)
    {
        return m_csense.analog_values[csense_channel];
    }
    
    ret_code_t nrf_drv_csense_sample(void)
    {
        ASSERT(m_csense.module_state == NRFX_DRV_STATE_POWERED_ON);
    
        if (m_csense.adc_channels_input_mask != 0)
        {
            if (m_csense.channels_to_read == 0)
            {
    #if USE_COMP == 0 && defined(SAADC_PRESENT)
                nrf_saadc_enable();
    #endif
                if (nrf_drv_csense_is_busy() == true)
                {
                    return NRF_ERROR_BUSY;
                }
                m_csense.busy = true;
                m_csense.channels_to_read = m_csense.adc_channels_input_mask;
                calculate_next_channel();
            }
    
    #if USE_COMP
            if (!m_csense.timers_powered_on)
            {
                nrf_drv_timer_enable(&m_timer0);
                nrf_drv_timer_enable(&m_timer1);
                m_csense.timers_powered_on = true;
            }
            else
            {
                nrf_drv_timer_resume(&m_timer0);
                nrf_drv_timer_resume(&m_timer1);
            }
            nrf_drv_comp_pin_select((nrf_comp_input_t)m_csense.cur_chann_idx);
    #ifdef SIMPLEXITY_CHANGES
            // we need the COMP events to be enabled when starting
            nrf_drv_comp_start(NRFX_COMP_EVT_EN_CROSS_MASK |
                               NRFX_COMP_EVT_EN_UP_MASK |
                               NRFX_COMP_EVT_EN_DOWN_MASK,
                               0);
            // toggle GPIO high to kick circuit into operation
            // Note, that since we have GPIO tasks enabled for this pin, setting the
            // raw output register is ignored, we need to use the task
            //
            if(nrfx_comp_sample()) // currently high, so set low
            {
                nrfx_gpiote_clr_task_trigger(NRF_CSENSE_OUTPUT_PIN);
            }
            else // currently low, so set high
            {
                nrfx_gpiote_set_task_trigger(NRF_CSENSE_OUTPUT_PIN);
            }
    #else
            nrf_drv_comp_start(0, 0);
    #endif
            
    #else
            ret_code_t err_code;
    #ifdef ADC_PRESENT
            adc_channel.config.config.ain = (nrf_adc_config_input_t)(1<<m_csense.cur_chann_idx);
            nrf_gpio_pin_clear(m_csense.output_pin);
            err_code = nrfx_adc_sample_convert(&adc_channel, NULL);
    #elif defined(SAADC_PRESENT)
            saadc_channel.pin_p = (nrf_saadc_input_t)(m_csense.cur_chann_idx + 1);
            nrf_saadc_channel_input_set(0, saadc_channel.pin_p, NRF_SAADC_INPUT_DISABLED);
            nrf_gpio_pin_clear(m_csense.output_pin);
            err_code = nrf_drv_saadc_sample();
    #endif //ADC_PRESENT
            if (err_code != NRF_SUCCESS)
            {
                return err_code;
            }
    #endif //USE_COMP
        }
    
        return NRF_SUCCESS;
    }
    
    bool nrf_drv_csense_is_busy(void)
    {
        return m_csense.busy;
    }
    #endif //NRF_MODULE_ENABLED(NRF_DRV_CSENSE)
    

  • Given that COMP: ISOURCE is not functional.  I was able to add a separate GPIO with series 1M resistor to stimulate the pad with the newer capacitor sensor driver.  This is the case where you set USE_COMP to 1.  I like this method because it uses MCU peripheral hardware to offload the CPU the most and can handle the complex timing.  I've attached the driver file from SDK16.0 with modifications to get the same ISOURCE functionality with a relaxation oscillator.