Here is an example of using an MP3V5010DP differential pressure sensor to detect expansion and contraction.
Mechanically, the device simply consists of two lengths of silicone tubing, one of which is sealed with a brass stub and connected to the positive port of the differential pressure sensor. The adjacent tube is open and partially joined to the sealed end of the other. The hoses are secured to the PCB with a suitably machined section of PVC square bar.
When something placed between the two lengths of silicone tube expands, force is applied to the sealed tube increasing its internal pressure, which is detected by the differential pressure sensor. Obviously these changes in pressure are only slight, and therefore an op-amp is used to scale-up the sensor's output voltage to a level that can be seen as a significant change when converted to a digital value by the PIC16LF18325's Analogue to Digital Convertor.
The device is powered using a CR2450 button cell battery. In order to reduce power consumption while in a sleep state an 'LF' designated PIC microcontroller is used, in combination with an N-channel MOSFET which controls power to the op-amp and pressure sensor.
On the top-side of the PCB is a status LED, and push button used to reset and configure the device. There's a 38KHz infrared LED for transmitting status data to a receiving device equipped with a suitably matched infrared sensor - such as a modified Magic Wand massager, for instance!
On the rear of the PCB are four pads for mounting an optional RF transmitter, such as the very reliable QAM-TX2-433 I use in my other projects. This was an experimental addition I included should I wish to control RF devices at some later stage.
Currently the device is programmed to transmit an NEC IR code of 0x19 when expansion is detected, and 0x45 when contraction, from the expanded state, is detected.
These two states are determined by first calculating an average from a set of readings taken from the Analogue to Digital Convertor - consisting of fourteen values, accumulated at two second intervals - and comparing it against an upper and lower threshold value.
An expansion state is set when the average value exceeds the upper threshold, and a contraction state when it drops below the lower threshold. This is very similar to the detection method used in my Hot Flow Activated Switch project.
The schematic is fairly self-explanatory. Power to the circuity in the blue-boxed section is controlled by the MOSFET Q2. This is so neither the op-amp or pressure sensor are consuming power when the microcontroller is in a sleep state.
Code listing for use with PIC16LF18325, compiled using MPLAB X IDE V5.15 and XC8 V2.10
/*
* File: pic_erection_detector.c
* Author: paul reynolds ( paul at v9tronics d0t c0m )
*
* Use averaging of the ADC input to detect a sudden change in pressure being
* applied to a MP3V5010DP pressure sensor - for detecting erections obviously!
*
* It's basically a rehash of the HFAS code, but with slower averaging and no
* relay to drive, we're just modulating an IR LED to transmit NEC IR commands.
*
* Created: MAY 2023
*/
// PIC16F18325 Configuration Bit Settings
// 'C' source line config statements
// CONFIG1
#pragma config FEXTOSC = OFF // FEXTOSC External Oscillator mode Selection bits (Oscillator not enabled)
#pragma config RSTOSC = HFINT1 // Power-up default value for COSC bits (HFINTOSC (1MHz))
#pragma config CLKOUTEN = OFF // Clock Out Enable bit (CLKOUT function is disabled; I/O or oscillator function on OSC2)
#pragma config CSWEN = ON // Clock Switch Enable bit (Writing to NOSC and NDIV is allowed)
#pragma config FCMEN = ON // Fail-Safe Clock Monitor Enable (Fail-Safe Clock Monitor is enabled)
// CONFIG2
#pragma config MCLRE = ON // Master Clear Enable bit (MCLR/VPP pin function is MCLR; Weak pull-up enabled)
#pragma config PWRTE = OFF // Power-up Timer Enable bit (PWRT disabled)
#pragma config WDTE = OFF // Watchdog Timer Enable bits (WDT enabled, SWDTEN is ignored)
#pragma config LPBOREN = ON // Low-power BOR enable bit
#pragma config BOREN = SLEEP // Brown-out Reset is enabled while running, disabled in Sleep
#pragma config BORV = LOW // Brown-out Reset Voltage selection bit (Brown-out voltage (Vbor) set to 2.45V)
#pragma config PPS1WAY = ON // PPSLOCK bit One-Way Set Enable bit (The PPSLOCK bit can be cleared and set only once; PPS registers remain locked after one clear/set cycle)
#pragma config STVREN = ON // Stack Overflow/Underflow Reset Enable bit (Stack Overflow or Underflow will cause a Reset)
#pragma config DEBUG = OFF // Debugger enable bit (Background debugger disabled)
// CONFIG3
#pragma config WRT = OFF // User NVM self-write protection bits (Write protection off)
#pragma config LVP = ON // Low Voltage Programming Enable bit (Low Voltage programming enabled. MCLR/VPP pin function is MCLR. MCLRE configuration bit is ignored.)
// CONFIG4
#pragma config CP = OFF // User NVM Program Memory Code Protection bit (User NVM code protection disabled)
#pragma config CPD = OFF // Data NVM Memory Code Protection bit (Data NVM code protection disabled)
#define _XTAL_FREQ 4000000
#include <xc.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* PIC16F18325
* [+++] [---]
* [RA5] [RA0]
* [RA4] [RA1]
* [RA3] [RA2]
* [RC5] [RC0]
* [RC4] [RC1]
* [RC3] [RC2]
*
* O RA0 > UART_TX_OUT (FOR DEBUGGING)
* I RA2 > SETUP BUTTON (INT)
* O RC1 > STATUS LED
* O RC5 > OUTPUT PIN (N/C)
* O RC4 > 2N7002: POWER TO LM358 / MP3V5010DP
* I RC3 > AN3: SIGNAL FROM LM358 / MP3V5010DP
*/
#define SET_BUTTON RA2
#define STATUS_LED RC1
#define SENSOR_PWR RC4
#define OUTPUT_PIN RC5
#define IR_38KHZ CCP1CONbits.CCP1EN
#define _ON_ 1
#define _OFF_ 0
#define START 1
#define STOP 0
#define TRUE 1
#define FALSE 0
#define PRESSED 0
int STORE_ADC_V;
int CURRENT_VAL;
int THE_AVERAGE;
int CYCLE_COUNT;
int counter_a = 0;
int counter_b = 0;
int counter_c = 0;
int avg_samples[14];
char serial_text[75];
int INITIALISE = 1;
int ERECTION = 0;
// these parameters will be loaded from EEPROM and can be adjusted by the user
// we're just setting them up here with the defaults so they can be loaded into
// the EEPROM when restoring default values.
int PARAM_DELAY_M;
int PARAM_TRIGGER;
int PARAM_RESTORE;
int PARAM_AVG_DUR;
int PARAM_VERBOSE;
//
// send magic wand remote control commands
//
unsigned int magic_wand_one = 0x45; // turn on, lowest intensity
unsigned int magic_wand_zero = 0x19; // turn off
void send_ir( int command, int tx_times ){
int command_tmp;
// prevent our transmission from being interrupted
INTCONbits.GIE = FALSE;
INTCONbits.PEIE = FALSE;
T0CON0bits.T0EN = FALSE;
// NEC remote address, and inverted-address
unsigned int address_one = 0x00;
unsigned int address_two = 0xFF;
TMR2ON = TRUE; // enable Timer2
for( int x = 0; x < tx_times; x++ ){
IR_38KHZ = TRUE; __delay_ms(9); // AGC pulse
IR_38KHZ = !IR_38KHZ; __delay_us(4500); // long pause
IR_38KHZ = !IR_38KHZ; __delay_us(550);
for( int yy = 0; yy < 4; yy++ ){
switch(yy){
case 1: command_tmp = address_one; // byte1: address
break;
case 2: command_tmp = address_two; // byte2: inverted-address
break;
case 3: command_tmp = command; // byte3: command
break;
case 4: command_tmp = ~command; // byte4: inverted-command
break;
}
for( int y = 0; y < 8; y++ ){
IR_38KHZ = !IR_38KHZ;
if( (command_tmp & 1) == 1 ){
__delay_us(1650);
}else{
__delay_us(550);
}
IR_38KHZ = !IR_38KHZ;
__delay_us(550);
command_tmp >>= 1; // shift one bit right
}
}
IR_38KHZ = FALSE; // disable CCP1EN (Capture Compare PWM) output
// pause between repeats - ideally 42 milliseconds to be valid NEC IR
for( int yy = 0; yy < 42; yy++ ){ __delay_ms(1); }
}
TMR2ON = FALSE;
// no longer transmitting, so interrupts are fine now
INTCONbits.GIE = TRUE;
INTCONbits.PEIE = TRUE;
T0CON0bits.T0EN = TRUE;
}
//
// for writing to the serial port when debugging
//
void tx_string( char * data_arr ){
unsigned int posi = 0;
unsigned int leng = strlen( data_arr );
while( posi < leng ){
while(!TRMT);
TXREG = data_arr[posi];
while(!TRMT);
__delay_ms(2);
posi ++;
}
}
//
// read value from analogue to digital converter
//
int get_adc_value(){
ADCON0bits.GO = 1; // start the conversion
while( ADCON0bits.GO == 1 ){ ; }; // wait for the conversion to end
int adc_value = (ADRESH<<8)+ADRESL; // combine the 10 bits of the conversion
return adc_value;
}
//
// add a sample to our array of fourteen samples
//
void add_a_sample( int *arr_values, int new_value ){
// shift values down one position
for(int i=14; i>0; i--){
arr_values[i] = arr_values[i-1];
}
// add new value to the beginning
arr_values[0] = new_value;
}
//
// calculate an average from our fourteen samples
//
int get_average( int *arr_values ){
int total = 0;
int number_of_numbers = 0;
for(int i=0; i<15; i++){
total = total + arr_values[i];
number_of_numbers++;
}
int result = total / number_of_numbers;
return result;
}
//
// initialise - called on startup and when pressing the button
//
void initialise( int *arr_values ){
// fill array with the current value
for(int i=0; i<14; i++){
arr_values[i] = CURRENT_VAL;
}
THE_AVERAGE = CURRENT_VAL;
STORE_ADC_V = TRUE; // flag to call average refresh below
OUTPUT_PIN = _OFF_;
__delay_ms(250);
}
//
// flash our status LED as required
//
void flash_led( int speed, int times ){
for(int i=1; i<=times; i++){
STATUS_LED = _ON_;
for(int y=0; y<=speed; y++){ __delay_ms(10); }
STATUS_LED = _OFF_;
for(int y=0; y<=speed; y++){ __delay_ms(10); }
}
}
//
// called to put PIC into sleep state
//
void pic_sleep(){
// shut down the serial port
SPEN = 0;
TXEN = 0;
// turn off power to the sensor
SENSOR_PWR = FALSE;
// disable ADC - otherwise this consumes power while in sleep
ADCON0bits.ADON = 0;
// indicate we're going to sleep
flash_led( 10, 2 );
// turn off high frequency oscillator
OSCENbits.HFOEN = 0;
asm("SLEEP");
}
//
// called after PIC has been woken
//
void pic_awake(){
OSCENbits.HFOEN = 1;
// enable serial port
SPEN = 1;
TXEN = 1;
// power to the sensor...
SENSOR_PWR = TRUE;
// and ADC
ADCON0bits.ADON = 1;
// flash once to indicate we're awake!
flash_led( 10, 1 );
}
//
// interrupt service routine
//
void __interrupt () my_isr_routine (void){
// a button press triggers INT
if( PIR0bits.INTF == 1 ){
INITIALISE = TRUE; // flag to call initialise routine
PIR0bits.INTF = 0; // clear interrupt flag
}
// timer zero adds a sample every x seconds
if( PIR0bits.TMR0IF == 1 ){
// add a sample PARAM_AVG_DUR * (15 = 1 second)
if( counter_a > (PARAM_AVG_DUR * 15) ){
STORE_ADC_V = TRUE;
counter_a = 0;
}
if( counter_a > 15 ){
// rapidly flash LED when an erection has been detected
if( ERECTION == TRUE ){
STATUS_LED = ~STATUS_LED;
}
}
counter_a ++;
PIR0bits.TMR0IF = 0; // clear interrupt flag
}
// IOC3/RA3
if( IOCAF3 == 1 ){
IOCAF3 = 0;
}
// IOC3/RA2
if( IOCAFbits.IOCAF2 == 1 ){
IOCAFbits.IOCAF2 = 0;
IOCANbits.IOCAN2 = 0;
PIR0bits.IOCIF = 0;
}
}
int main(){
// OSCILLATOR @ 4MHz
OSCCON1bits.NOSC = 0b110; // HFINTOSC (1MHz)
OSCCON1bits.NDIV = 0b0000; // divide by 1
OSCFRQbits.HFFRQ = 0b0011; // 4MHz (1MHz = 0b0000)
OSCENbits.HFOEN = 1; // enable high-frequency oscillator
CPUDOZEbits.IDLEN = 0; // sleep instruction places the device into full-sleep mode
VREGCONbits.VREGPM = 1; // low-power sleep mode enabled in sleep
// PIN SETUP
TRISA = 0b00000100; // outputs, except RA2 button
ANSELA = 0b00000000; // all digital pins
TRISC = 0b00001000; // outputs, except RC3 2N7002 to power MP3V5010DP / LM358
ANSELC = 0b00001000; // all digital pins, except RC3/ANC3 MP3V5010DP / LM358
// WEAK PULL UPS
WPUAbits.WPUA2 = 1; // enable weak pull up on RA2 (BTN)
WPUAbits.WPUA3 = 1; // enable weak pull up on RA3 (MCLR)
// UART CONFIG
SYNC = 0; // disable for asynchronous EUART
SPEN = 0; // enables the EUSART and sets TX/CK I/O pin as output
TXEN = 0; // enables transmitter
RA0PPS = 0b10100; // peripheral pin setup - UART TX is on RA0 (ICSP:PIN4)
CREN = 0; // enables receiver
TX9 = 0; // set to 8bit transmit
RX9 = 0; // set to 8bit receive
// UART BAUD RATE
BRGH = 1; // baud rate generator - high baud rate
BRG16 = 1; // baud rate generator - 8 bit operation
SPBRG = 103; // baud rate generator - 9600@4MHz
SENDB = 1; // send break character
// INTERRUPT SETUP
INTCONbits.INTEDG = 0; // interrupt on INT falling edge
PIR0bits.INTF = 0; // reset INT interrupt flag
PIE0bits.INTE = 1; // /./enable INT interrupt
PIE0bits.IOCIE = 1; // enable IOC interrupt
IOCANbits.IOCAN2 = 1; // enable IOC negative edge detection on RA2
INTCONbits.GIE = 1; // global interrupt enable
INTCONbits.PEIE = 1; // peripheral interrupts enable
// ADC SETUP
ADCON1bits.ADFM = 1; // ADC result is right justified
ADCON1bits.ADNREF = 0; // negative reference to ground
ADCON1bits.ADPREF = 0b00; // V REF + is connected to VDD
ADCON1bits.ADCS = 0b101; // Fosc/2 is the conversion clock
ADCON0bits.CHS = 0b010011; // select analog input, ANC3/RC3
ADCON0bits.ADON = 0; // turn on adc
// TMR0 SETUP
T0CON0bits.T0EN = 1; // timer 0 enabled
T0CON0bits.T016BIT = 0; // an 8 bit timer
T0CON0bits.T0OUTPS = 0; // 1:1 post-scaler
T0CON1bits.T0CS = 0b010; // ? Fosc / 4 ????
T0CON1bits.T0CKPS = 0b1000; // pre-scaler 1:256
T0ASYNC = 0;
TMR0L = 0;
TMR0IF = 0; // clear the Timer 0 interrupt flag
TMR0 = 0; // load a value of 0 into the timer
TMR0IE = 1; // enable the Timer 0 interrupt
// CCP1 PWM configuration - for modulating our IR LED at 38kHz
RC2PPS = 0b01100; // RC2 source is CCP1
PR2 = 25; // 76.92 kHz @ 8MHz
CCP1CONbits.CCP1MODE = 0b1111; // PWM mode
CCP1CONbits.CCP1EN = 0; // turn CCP1 on/off
CCPR1L = 48; // 53% duty cycle - make 50% at some point
CCPR1H = 0;
CCPTMRSbits.C1TSEL = 0b01; // CCP1 Capture, Compare and PWM mode on Timer2
// TMR2 configuration
T2CONbits.T2CKPS = 0b00; // 1:1 prescaler
T2CONbits.T2OUTPS = 0b0000; // 1:1 postscaler
T2CONbits.TMR2ON = 0; // turn timer2 off - enable when required
TMR2IE = 0;
TMR2IF = 0;
//
// default parameters
//
PARAM_DELAY_M = 1; // just one minute
PARAM_TRIGGER = 10; // trigger when cur_value is x points above average
PARAM_RESTORE = 8; // restore when cur_value is x points below average
PARAM_AVG_DUR = 2; // take a sample for averaging every x seconds
PARAM_VERBOSE = 1; // transmit debug information via serial port
//
// if EEPROM hasn't been written to yet, or we've powered-on
// with the button pressed, write the default parameters and restart
//
if( eeprom_read( 0x10 ) != 5 || SET_BUTTON == 0 ){
for( int x = 0; x <= 5; x++ ){
STATUS_LED = ~STATUS_LED;
__delay_ms(200);
}
STATUS_LED = 0;
flash_led(200,5);
eeprom_write( 0x10, 5 );
eeprom_write( 0x02, PARAM_DELAY_M );
eeprom_write( 0x03, PARAM_TRIGGER );
eeprom_write( 0x04, PARAM_RESTORE );
eeprom_write( 0x06, PARAM_AVG_DUR );
eeprom_write( 0x07, PARAM_VERBOSE );
__delay_ms(500);
RESET();
}
//
// initialise parameters stored in EEPROM
//
PARAM_DELAY_M = eeprom_read( 0x02 );
PARAM_TRIGGER = eeprom_read( 0x03 );
PARAM_RESTORE = eeprom_read( 0x04 );
PARAM_AVG_DUR = eeprom_read( 0x06 );
PARAM_VERBOSE = eeprom_read( 0x07 );
//
// variables used when pressing the button / adjusting parameters
//
int btn_press_counter = 0;
int btn_press = 0;
int btn_abort_counter = 0;
//
// only enable the serial port if it's required
//
if( PARAM_VERBOSE ){
SPEN = 1; // enables the EUSART and sets TX/CK I/O pin as output
TXEN = 1; // enables transmitter
}
//
// once powered on go to sleep
//
pic_sleep();
pic_awake();
while(1){
CURRENT_VAL = get_adc_value();
//
// add a new sample for averaging - called by timer-zero interrupt
//
if( STORE_ADC_V == TRUE ){
add_a_sample( avg_samples, CURRENT_VAL );
THE_AVERAGE = get_average( avg_samples );
STORE_ADC_V = FALSE;
}
//
// send values and parameters to serial port - if enabled
//
if( PARAM_VERBOSE == 1 ){
sprintf( serial_text, " AVG %d CUR %d ERECT %d CYCLE %d [T+%d R-%d AVG%d]\r\n",
THE_AVERAGE, CURRENT_VAL, ERECTION, CYCLE_COUNT, PARAM_TRIGGER, PARAM_RESTORE, PARAM_AVG_DUR );
tx_string( serial_text );
}
//
// reset averaging etc.
//
if( INITIALISE ){
initialise( avg_samples );
INITIALISE = 0;
}
// if the ADC input has RISEN SIGNIFICANTLY above average then
// turn ON the output, but not if it's already ON
if ( ( CURRENT_VAL - PARAM_TRIGGER ) > THE_AVERAGE ){
/////////////////////////////////////////
/// ERECTION DETECTED / TURN WAND OFF ///
/////////////////////////////////////////
if( OUTPUT_PIN == _OFF_ ){
OUTPUT_PIN = _ON_;
ERECTION = TRUE;
if( PARAM_VERBOSE ){
sprintf( serial_text, " TURNING OFF IN... ");
tx_string( serial_text );
}
// on every even/other COUNT_CYCLE turn wand off after 4 seconds
if( CYCLE_COUNT % 2 == 0){
// 4 sec delay / flash LED
for(int i=1; i<=4; i++){
STATUS_LED = _ON_;
__delay_ms(500);
STATUS_LED = _OFF_;
__delay_ms(500);
if( PARAM_VERBOSE ){
sprintf( serial_text, "%d ", i );
tx_string( serial_text );
}
// escape loop if the button has been pressed
if( INITIALISE ){ i = 1000; }
}
}
if( PARAM_VERBOSE ){
sprintf( serial_text, " NOW! \r\n");
tx_string( serial_text );
}
send_ir( magic_wand_zero, 10 ); // turn off the MW, TX*10
}
}
// if the ADC input goes SIGNIFICANTLY BELOW AVERAGE then
// turn OFF the output, but not if it's already OFF
if( ( CURRENT_VAL + PARAM_RESTORE ) < THE_AVERAGE ){
////////////////////////////////////
/// ERECTION LOST / TURN WAND ON ///
////////////////////////////////////
if( OUTPUT_PIN == _ON_ ){
OUTPUT_PIN = _OFF_;
ERECTION = FALSE;
if( PARAM_VERBOSE ){
sprintf( serial_text, " TURNING ON IN... ");
tx_string( serial_text );
}
// delay gradually gets longer with every on/off cycle
for(int i=1; i<=(10+CYCLE_COUNT)*2; i++){
STATUS_LED = _ON_;
__delay_ms(500);
STATUS_LED = _OFF_;
__delay_ms(500);
if( PARAM_VERBOSE ){
sprintf( serial_text, "%d ", i );
tx_string( serial_text );
}
// escape loop if the button has been pressed
if( INITIALISE ){ i = 1000; }
}
if( PARAM_VERBOSE ){
sprintf( serial_text, " NOW! \r\n");
tx_string( serial_text );
}
send_ir( magic_wand_one, 3 ); // turns on the MW, TX*3
CYCLE_COUNT ++; // increment cycles since reset
}
}
__delay_ms(250);
//
// respond to button presses
//
while( SET_BUTTON == PRESSED ){
// one press sends an off command and resets averaging
STATUS_LED = _OFF_;
ERECTION = FALSE;
CYCLE_COUNT = 0;
send_ir( magic_wand_one, 3 ); // turns on the MW
initialise( avg_samples );
if( PARAM_VERBOSE ){
sprintf( serial_text, " 1) RESET! \r\n");
tx_string( serial_text );
}
flash_led( 300, 1 ); // once to confirm option #1
btn_press = 1;
btn_press_counter = 0;
// select an option by holding the button and counting flashes
while( SET_BUTTON == PRESSED ){
btn_press_counter ++;
if( btn_press_counter > 10000 ){
flash_led( 300, 1 );
btn_press ++;
btn_press_counter = 0;
}
}
while( btn_press > 0 ){
if( btn_press == 2 ){
// enter sleep state...
if( PARAM_VERBOSE ){
sprintf( serial_text, " 2) SLEEP! \r\n");
tx_string( serial_text );
}
flash_led( 300, 2 ); // twice to confirm option #2
pic_sleep();
pic_awake();
}
if( btn_press == 3 ){
// does nothing as yet
if( PARAM_VERBOSE ){
sprintf( serial_text, " 3) DOES NOTHING YET! \r\n");
tx_string( serial_text );
}
flash_led( 300, 3 ); // thrice to confirm option #3
}
//
// Later we'll add options here for changing operating parameters
//
btn_press = 0;
break;
}
}
//end while
}
// end main
}