/* Mods for the Arduino Nano Pulse Induction Metal Detector.
* 1 - to drive the Tx and mainSample signals by PWM
* 2 - to modulate the frequency according to target strength
* 3 - OLED interface to visulaize the delay
*
* Version: 2.0.5Encoder
* Date: Feb 24 2024
* Author: Teleno
*
* Changes in 2.0.5EncoderBoost:
* Delay potentiometer replaced with dual purpose encoder with push button for TX power and main sample delay adjustments. Parameter selection by push button.
* Changes in 1.0.5:
* - Added low-pass IIR filter for the pot reading, reducing noise substantially. Less hysteresis is necessary,
* allowing a larger delay range.
* This requires a higer refresh rate for the pot, so it's now read 1000 per second.
* Changes in 1.0.4:
* - TX Boost icon added to the FontLargeDigits.h file, code for displaying it on switch changes.
* Changes in 1.0.3:
* - Pot reading validated against adaptive hysteresis thresholds before being accepted (+- 8 bit noise).
* Changes in 1.0.2:
* - Mapped pot reading no longer added to defMainDelay but constrained between defMainDelay and maxMainDelay.
* - Errata fixed.
* Changes in 1.0.1:
* - Pot filtering by Arduino map() function. User editable delay range (defMainDelay, defMaxDelay)
* - Errata fixed.
*
* Credits:
* - Qiaozhi (George) for the original version of the sketch.
* - Gunghouk for encoder modifications based on Ralph Bacon's adaptation of an idea by Marko Pinteric
* - SaltyDog for field testing and feedback.
*
* - Interrupt-based Rotary Encoder from a technical paper by Marko Pinteric
* https://www.pinteric.com/rotary.html
* Subsequently adapted by Ralph Bacon then re-adapted by Gunghouk
*
* Mods to the PCB:
* - MOD1 - Essential: This sketch won't work without this mod. See "PCB_mods.png" for the details.
*
* - MOD2 - Only if you want audio with frequency and amplitude modulation. See "PCB_mods.png" for the details.
* see https://www.geotech1.com/forums/forum/projects/arduino-nano-pi/420019-modified-sketch-for-improved-timing-precision?p=420165#post420165
*
*
* If you don't use the OLED comment out the "#define USE_OLED" line below and rebuild.
* Otherwise the Nano will wait forever for a display to be ready that doesn't exist.
*
* See comments in file "Oled.h" for instructions to get the OLED working.
*
* If you don't use the encoder comment out the #define USE_ENCODER line and rebuild.
*/
//*******************************************************************
//*** BEGIN OF USER-EDITABLE PARAMETERS ****************************
//*******************************************************************
// Uncomment the extra features you want to use below **********
//*******************************************************************
//
#define USE_OLED // Instructions in file "Oled.h"
//#define USE_FREQ_MODULATION // requires MOD2 of PCB!!!!!
// IMPORTANT NOTICE : if you uncomment USE_OLED but there's no physical OLED display
// attached, the MCU will block waiting for a non exiting display to respond.
volatile bool rotaryEncoder = false; // encoder has changed
/**** Detector timings in microseconds ****/
// For exact timings, edit using only multiples of 4 us for all parameters.
// Exception: defMainDelay can take any value >= 1us.
#define normalPower 52 // Normal TX-on time (52us), multiples of 4us
#define maxBoostPower 100 // Boost TX-on time (100us), multiples of 4us
#define defMainDelay 1 // Default main sample delay (1us) >= 1us
#define defMaxDelay 100 // Default maximum main sample delay.
#define mainSample 48 // Main sample pulse width (48us) >= 4us, multiples of 4us
#define efeDelay 240 // EFE sample pulse delay (240us)
#define efeSample mainSample // EFE sample pulse width (same as mainSample), multiples of 4us
#define txPeriod 1000 // Repetition rate of the pulse train (<= 1000), multiples of 4us.
int boostIncSize = 4 ; // Boost increment increment size
int delayIncSize = 1;
bool useEncoder = false;
int rotationCounter = defMainDelay; // set encoder delay counter to minimum delay uS
volatile int8_t rotationValue;
int boostPower = maxBoostPower; // Boost TX-on time (100us), multiples of 4us
int lastBoostPower;
int boostCounter = normalPower; // set encoder boost counter to minimum (normal) power
//volatile int8_t boostValue;
//*** Define your preferred range of audio frequencies ***
// (requires PCB modification MOD2)
#ifdef USE_FREQ_MODULATION
#define targetPin A1 // Assign integrator output (TP10) to A1 (requires PCB modification MOD2)
#define FREQ_MIN 440 // minimum and maximum audio frequencies in Hz
#define FREQ_MAX 880
#endif
//*******************************************************************
//*** END OF USER-EDITABLE PARAMETERS ******************************
//*******************************************************************
// Pin assignments.
// The pins for Tx pulse, main sample and EFE sample are fixed
// Tx : pin 10
// main sample : pin 9
// EFE : pin 5
#define audioPin 11 // Assign pin 11 to audio chopper
#define boostPin 12 // Assign pin 12 to boost switch/encoder push button
#define delayPin A0 // Assign delay pot to A0
// encoder pins
#define PIN_A 2 // CLK/A/S1 etc
#define PIN_B 3 // DT/B/S2 etc
// Don't edit or move these lines upwards of the above statements.
#include "Timers.h"
#ifdef USE_OLED
#include "Oled.h"
#endif
void setup() {
// put your setup code here, to run once:
pinMode(boostPin, INPUT_PULLUP); // Set Boost switch pin to input mode with pullup resistor
Timers_init(); // Configure and start timers
pinMode(PIN_A, INPUT_PULLUP); // Set encoder A & B pins to input mode with pullup resistors
pinMode(PIN_B, INPUT_PULLUP);
attachInterrupt(0,rotary,CHANGE); // set an interrupt on PinA
attachInterrupt(1,rotary,CHANGE); // set an interrupt on PinB
useEncoder = true;
#ifdef USE_OLED
u8x8_init(); // Initialize OLED display
//u8x8_inverse();
//oled.on();
#endif
}
void rotary() // interrupt on encoder change
{
rotaryEncoder = true;
}
int8_t checkRotaryEncoder()
{
// Reset the flag that brought us here (from ISR)
rotaryEncoder = false;
static uint8_t lrmem = 3;
static int lrsum = 0;
static int8_t TRANS[] = {0, -1, 1, 14, 1, 0, 14, -1, -1, 14, 0, 1, 14, 1, -1, 0};
static int8_t returned = 0;
// Read BOTH pin states to deterimine validity of rotation (ie not just switch bounce)
int8_t l = digitalRead(PIN_A);
int8_t r = digitalRead(PIN_B);
// Move previous value 2 bits to the left and add in our new values
lrmem = ((lrmem & 0x03) << 2) + 2 * l + r;
// Convert the bit pattern to a movement indicator (14 = impossible, ie switch bounce)
lrsum += TRANS[lrmem];
// encoder not in the neutral (detent) state
if (lrsum % 4 != 0)
{
return 0;
}
// encoder in the neutral state - clockwise or anti-clockwise rotation
if (lrsum == 4)
{
lrsum = 0;
return 1;
}
// encoder in the neutral state - anti-clockwise rotation
if (lrsum == -4)
{
lrsum = 0;
return -1;
}
// An impossible rotation has been detected - ignore the movement
lrsum = 0;
return 0;
}
void loop() {
// put your main code here, to run repeatedly:
#ifdef USE_FREQ_MODULATION
word target = analogRead(targetPin);
freq = map(target, 0, 635, FREQ_MIN, FREQ_MAX);
freq = constrain(freq, FREQ_MIN, FREQ_MAX);
OCR2A = TIMER2_CLK / freq - 1;
#endif
#ifdef USE_OLED
// Display the new delay or TX value (us)
if (rotationCounter != delayVal_last || boostPower != lastBoostPower || boost != last_boost) {
u8x8_invert(rotationCounter, boostPower, boost) ;
delayVal_last = rotationCounter;
last_boost = boost;
lastBoostPower = boostPower;
}
#endif
// Has rotary encoder moved?
if (rotaryEncoder) {
// Get the movement (if valid)
int8_t rotationValue = checkRotaryEncoder();
if (!boost) // if not in boost mode adjust delay else adjust boost
{
// If valid movement, do something
switch (rotationValue)
{
case 1: // clockwise
rotationCounter += delayIncSize ;
if (rotationCounter > defMaxDelay) rotationCounter = defMainDelay;
break;
case -1: // anticlockwise
rotationCounter -= delayIncSize ;
if (rotationCounter < defMainDelay) rotationCounter = defMaxDelay;
break;
default: // invalid value or no movement
break;
}
}
else
{
// If valid movement, do something
switch (rotationValue)
{
case 1: // clockwise
boostCounter += boostIncSize ;
if (boostCounter > maxBoostPower) boostCounter = normalPower; // wrap around range check
break;
case -1: // anticlockwise
boostCounter -= boostIncSize ;
if (boostCounter < normalPower) boostCounter = maxBoostPower; // wrap around range check
break;
default: // invalid value or no movement
break;
}
}
}
else
{
if (digitalRead(boostPin) == LOW)
{
boost = !boost;
// Wait until button released (demo only! Blocking call!)
while (digitalRead(boostPin) == LOW)
{
delay(1);
}
}
}
}