This library can be used for building proximity sensors by detecting a change in capacitance when an user is near or touching a sensor. Although multiple sensors are supported, the library is designed mainly for single sensor use and the goal was to keep it small enough to fit an ATiny13 micro-controller. For multiple button readings, another algorithm must be used based on similar principles discussed here.
A sensor needs two pins, one 2-10M resistor and a watchdog timer. The watchdog timer was used since is not used by most people and it saves one 8-bit timer.
Contents
- Basics of capacitive sensing
- Physical sensor layout design
- Capacitive sensing applications
- Sensor connection diagram
- Software implementation for a capacitive sensor
- API
- Code example
- Links
Basics of capacitive sensing
Capacitive sensors can be build using pads on a printed circuit board or some other conductive materials such as copper tape or aluminium foil. For large sensors, the area must not necessarily be filled, instead a loop of wire can be used.
|
| Microchip AN1492 |
When an object with a different dielectric than air, such as a user hand, enters the sensor's proximity, the capacity is added in parallel to the baseline capacitance of the sensor thus increasing the overall measured capacitance.
Capacitive proximity sensors work similar as capacitive touch sensors with the main difference that the proximity signal shift will be significantly smaller than a touch signal, because it must work over long distances through air rather than plastic or glass. To maintain a reliable detection, the system needs to keep a good Signal-to-Noise Ratio (SNR). So proximity applications require more careful system design considerations.
|
| Texas Instruments FDC1004 |
Physical sensor layout design
To maximize the sensor range detection maximize the distance of the sensor to a ground plane. Ground planes have two effects on the proximity. First, the ground plane will block the proximity sensor from seeing an approaching object if it is placed in its path. In free space, a sensor can emit its electric field freely in all directions with little attenuation. When a ground plane is introduced, the electric field lines emitting from the sensor want to terminate on the ground plane. As the distance between the ground and the sensor decreases, the strength of the field radiating decreases. When a ground plane is placed closer to the sensor, the sensing range is reduced.
Second, ground planes will increase the base capacitance when directly below or adjacent to the proximity sensor. This decreases the percentage of change seen in the signal when an object approaches, which reduces the sensitivity. However a hatched ground plane can still be useful underneath the sensor to shield from unwanted noise.
Capacitive sensing applications
Capacitive sensing can be used for touch or gesture applications such as touch lamps, capacitive sliders and buttons, liquid level sensing or material analysis that monitors a stack of papers.
|
| Texas Instruments FDC1004 |
Their main advantage is the cheap construction and since they don't require an actuator to be pushed they can be easily encapsulated in a waterproof container. The disadvantage is that a more careful design is needed to prevent false triggering by environmental noise. In case of a touch button another issue would be water or ketchup for example remaining on the sensor surface.
Sensor connection diagram
For a capacitive sensor two pins are needed and any microcontroller pin can be used. The Signal pin is used to charge the capacitor which is the sensor, through a 2 to 10M resistor. The Sensor pin will discharge the capacitor and also detect when the capacitor is considered charged or discharged. The resistor R2 is optional but recommended to protect the pin from ESD. The value can be somewhere around 100 - 150 ohm to limit the current through the internal clamping diodes.
![]() |
| Single sensor |
![]() |
| Multiple sensors |
Software implementation for a capacitive sensor
The first stage in detecting a change in capacitance is to measure the capacitance. Measuring the absolute capacitance in Farads would take memory and CPU cycles unnecessary since we only need to know the relative capacitance. We can use the charge time for this. When an object is approaching the sensor, the capacitance increases thus the charge time will increase. We don't even need to know the absolute time in seconds but the relative time. This way, a timer is not needed. To measure the relative time we can count the CPU cycles while the capacitor is charging. Since the capacitance of a sensor is in order of pico-farads, the charge time will be a few tens of microseconds.
Measuring the charge time
The Sensor pin is set as input and the Signal pin to output high to charge the capacitor through the resistor. A while loop will run as long as the Sensor pin is low and during this time, the int16_t raw variable is incremented. The interrupt are disabled during the loop to prevent loss of counting. After the while loop the capacitor is considered charged. Next, both Sensor and Signal pins are set as output low to discharge the capacitor.
Averaging
An int16_t variable called average is used to hold an average of the last 8 samples. When visualized in a plotter application this is the raw signal but much smoother and a bit behind in time.
The average is calculated as follows.
average = average + (raw - average) / 8
The 8 represents the number of samples. While experimenting I've noticed that using 16 will make the sensor less sensitive because the average value will increase being too close under the raw signal. Using 4 will make the average signal less smooth.
To increase the sensitivity, the average could be taken every 8 samples for example but that could lead to false triggering by environmental noise.
Tripping value
An object is considered detected when the raw signal is greater than the average plus the tripping value. The trip value can be found by trial and error or using UART and a serial plotter such as https://github.com/hyOzd/serialplot.
When the raw signal is higher than average plus trip value, the sensing_count variable is incremented.
Time window
The watchdog timer is set to trigger an interrupt that increments a variable every 16ms. This is the lowest time resolution of the WDT. The timer is used to "de-bounce" the sensor and also to detect a long press.
At a predefined interval of say 50ms, the variable sensing_count is compared against another trip_on value. So during the 50ms interval the sensing_count should be high enough for an object to be considered detected. Setting the trip_on higher will prevent false triggering but too high of a value will decrease the sensitivity.
Another time window is used to detect when the object is removed. This time the window can be larger, for example 100ms for more reliability. Again, the sensing_count is compared against a trip_off value but this time the sensing_count should be ideally zero. Do to noise and the object crossing the trip boundary, the sensing_count will most likely be a positive value but very small when the object is departing or the system is idle, compared to when an object is near the sensor.
Using another time window a long press can be detected or even a "stuck" button when for example the system detects that the button was touched for more than 10 seconds.
Detection example
Using an UART to USB adapter and a serial plotter software we can visualize how the signals look during a detection.
|
| SerialPlot - Object detection-Raw |
Illustrated above is so called raw variable that is the "time" the capacitor charges. The signal increase in the middle is when the hand was approaching the sensor.
|
| SerialPlot - Object detection-Average |
The average signal is based on the raw signal and is moves slower. The idea is to keep it stable so that the raw signal can increase above it to make a detection.
|
| SerialPlot - Object detection-sensing_count |
The variable sensing_count is incremented each time the raw signal is above the average plus the trip value. Here the dynamic range looks good. The number is small when no object is detected and large when a hand approaches the sensor.
|
| SerialPlot - Object detection-State |
API
Configuration
The header file contains some parameters that can be modified.
#define SENSOR_DEBUG 0
When SENSOR_DEBUG is set to 1, the UART library will be included. Inside the function prixiSense there are some UART functions used to send some variables to a serial adapter. A serial plotter such as https://github.com/hyOzd/serialplot, can be used for sensor calibration and debugging.
#define SENSOR_LONGPRESS_TIME (1000.0 / SENSOR_WDTIMER_RES_MS) // [default: 1000]
The SENSOR_LONGPRESS_TIME is used to define the time in milliseconds for a button to be considered long pressed.
#define SENSOR_SENSING_ON_TIME (50.0 / SENSOR_WDTIMER_RES_MS) // [default:50]
SENSOR_SENSING_ON_TIME is the time window in milliseconds in which an object is detected. Usually 50ms works fine.
#define SENSOR_SENSING_OFF_TIME (100.0 / SENSOR_WDTIMER_RES_MS) // [default:50]
SENSOR_SENSING_OFF_TIME is the time window in milliseconds in which an object is considered removed.
#define SENSOR_ON_COUNT 3 // [default: 3]
SENSOR_ON_COUNT is used for de-bouncing. Indicates how many times the object is detected in a given time frame. Greater value prevents false triggering but lowers sensitivity. Proper value depends on sensor capacity and can be found by trial and error and/or using a serial plotter and UART.
#define SENSOR_OFF_COUNT 3 // [default:3]
SENSOR_OFF_COUNT is same as above but this time for detecting when an object is removed. Environmental noise can cause false triggering. Setting this to lower values such as 0 or 1 will reduce sensitivity or even block it since there will still be some detection even when the object is farther away.
#define MAX_NR_OF_SENSORS 1
MAX_NR_OF_SENSORS: maximum number of sensors. Increasing this will increase code size. Added sensors can be less than this.
Library structure
Sensor state enumerator
typedef enum { IDLE = 0, OBJECT_DETECTED, } SENSOR_STATE;
Idle state is when no detection is made.
proxiSense structure
typedef struct { void (*setSignalPin_f)(bool high); // Pointer to a function that configures the common OUT pin void (*setSensorPin_f)(bool input); // Pointer to a function that sets the IN pin uint8_t (*readSensorPin_f)(void); // Pointer to a function that reads the IN pin uint8_t id; // Sensor ID uint8_t long_pressed; SENSOR_STATE sensing_stage; uint16_t sample_nr; uint16_t sensing_count; uint16_t time_on; uint16_t time_on_stuck; uint16_t time_long_press; proxiSenseSize_t raw; proxiSenseSize_t average; } proxiSense_t;
id: can be used to identify the sensor when multiple sensor are used.
Initialization
proxiSense_t* proxiSense_init(void (*setSignalPin_f)(bool high), void (*setSensorPin_f)(bool input), uint8_t (*readSensorPin_f)(void))
The function returns a pointer to a proxiSense object or NULL if there is no more space in the allocated array.
Since there could be multiple sensors, the function takes three function pointers as arguments. These functions must be defined by the user and are used to drive the pins. There is a template in the download section that can be used. The only things that should be changed is the port letter and pin number for every sensor. For multiple sensors, duplicate the 3 functions for each extra sensor and replace the numbers in function names to indicate to which sensor it belongs.
Read sensor
SENSOR_STATE proxiSense(proxiSense_t* sensor, uint16_t sensitivity)
Main function used to read a sensor.
proxiSense_t* sensor
Pointer to a proxiSense object.
uint16_t trip_value
A trip value that sets the trigger level and sensitivity.
Return
Returns the sensor state of proxiSenseSize_t enum type.
Read raw value
proxiSenseSize_t proxiSense_chargeValue(proxiSense_t* sensor)
Returns the raw variable that is incremented during the charge time of the capacitor. Used by liquid level measurement.
Detect a long press
uint8_t proxiSense_longPress(proxiSense_t* sensor)
Returns 1 if long press otherwise 0. Once read the flag is reset and the long press time must expire for the flag to be set again.
Code example
#include "proxiSense.h" #include "uart.h" int main(void){ uint16_t sensor1_state = 0; uint8_t s1_last_state = 0; UART_begin(&uart0, 115200, UART_ASYNC, UART_NO_PARITY, UART_8_BIT); UART_sendString(&uart0, "System started.\n"); // sensor_SetSignalPin, sensor1_SetSensorPin, sensor1_ReadSensorPin defined by "proxiSensePins_devBoard.h" proxiSense_t* sensor1 = proxiSense_init(sensor_SetSignalPin, sensor1_SetSensorPin, sensor1_ReadSensorPin); while(1){ sensor1_state = proxiSense(sensor1, 4); if(proxiSense_longPress(sensor1)){ UART_sendString(&uart0, "Sensor 1 long press\n "); } // Run only if sensor state changes if(sensor1_state != s1_last_state){ s1_last_state = sensor1_state; UART_sendString(&uart0, "Sensor 1: "); UART_sendInt(&uart0, sensor1_state); UART_sendString(&uart0, "\n"); } } return(EXIT_SUCCESS); }
Links
| The link also includes the UART library useful for debugging or calibration. | |
| v1.0 | proxiSense |
Changelog |
|
| v1.0 (2025-10-24) | Public release under GNU GPL v3 license. |
References |
|
|
Microchip AN1492 Capacitive Proximity Design Guide
|
|
|
Texas Instruments FDC1004 Basics of Capacitive Sensing and Applications
|
|
Resources |
|
| A series of 10 videos summing around 5 hours. The series is about liquid level sensing, and covers in-depth subjects related to capacitive sensing
such as different ways to measure capacitance and the dielectric constant. Robert's Smorgasbord - Capacitive Liquid Level Sensing |
|
|
Simply Put - How A Dialectric Works and How Capacitive Proximity Sensors Work
|
|









No comments:
Post a Comment