/* Description: This program evaluates the pulse length of an oscillator, whose frequency is determined by a coil with a movable ferrite core. The movable ferrite core simulates the bolt of a lock, which enters a sensor coil - that means: the whole circuit works as an inductive proximity sensor, detecting the lock-state of a lock (unlocked/intermediate/locked): The result is displayed on an OLED-display as well as on an LED string (similar to a traffic light. A calibration button hooked to the nano can be used to calibrate min/max values of the osczillator's pulse length. The whole project (including the oscillator) is described in the following thread www.mikrocontroller.net (in german language): https://www.mikrocontroller.net/topic/564564?goto=new#new Version: v006e - released on 07.05.2024 Author: Igel1 Hardware used: - Arduino Board (tested with Nano, but other boards should also work) * https://docs.arduino.cc/hardware/nano/ * https://devboards.info/boards/arduino-nano - Oscillator Circuit * https://www.mikrocontroller.net/topic/564564?page=single#7631750 * https://www.mikrocontroller.net/topic/564564?page=single#7632514 - String of 3 NeoPixels * https://www.adafruit.com/product/1612 * https://github.com/adafruit/Adafruit_NeoPixel - 2.42" OLED Display Module 128x64 with SSD1309 Controller * https://de.aliexpress.com/item/1005006473260235.html * https://www.mikrocontroller.net/topic/566536#new * https://wokwi.com/projects/376501375827109889 Specification & Vendor Information: ----------------------------------- OLED - Spec: 2.42 inch 2.42" OLED Display Module 128x64 LCD HD Screen Module SSD1309 7 Pin SPI/IIC I2C Serial Interface for Arduino UNO R3 OLED - Vendor: https://de.aliexpress.com/item/1005006473260235.html Recommended Readings: --------------------- Hardware considerations (MC - Thread): https://www.mikrocontroller.net/topic/566536?goto=7642989#7642989 Burning Bootloaders: https://forum.arduino.cc/t/how-to-burn-boot-loader-of-arduino-uno-r3-usin-aurdino-mega-2650/320626 Library for monochrome displays, v2: https://github.com/olikraus/u8g2 Sample code for using u8g2: https://wokwi.com/projects/376501375827109889 Very good Pinouts for all Arduinos: https://devboards.info/ PWM: https://www.arduino.cc/reference/en/language/functions/analog-io/analogwrite/ Arduino Boards (only Nano v3 was fully tested): ---------------------- Arduino Uno R3: https://store.arduino.cc/products/arduino-uno-rev3 (Product Page) (not testet yet) https://docs.arduino.cc/hardware/uno-rev3/ (Documentation) Arduino Nano v3: https://store.arduino.cc/products/arduino-nano (Product Page) https://docs.arduino.cc/hardware/nano/ (Documentation) Arduino Mega 2560 Rev3: https://store.arduino.cc/products/arduino-mega-2560-rev3 (Product Page) https://docs.arduino.cc/hardware/mega-2560/ (Documentation) Sparkfun Pro Micro - 5V/16 MHz: https://www.sparkfun.com/products/12640 (Product Page) https://learn.sparkfun.com/tutorials/pro-micro--fio-v3-hookup-guide (Documentation) Arduino Pro Mini (Clone from deep-robot): https://www.pixelelectric.com/development-boards/arduino/boards/arduino-pro-mini-5v-16mhz/ (Product Page) (not tested yet) Datasheets OLED Display and surrounding Hardware: -------------------------------------------------- X6206 - Voltage Regulator: https://product.torexsemi.com/system/files/series/xc6206.pdf HM1308 - Step-up DC/DC Converter: https://www.hmsemi.com/index.php/Down/down/id/1519.html SSD1309 - OLED Driver with Controller: https://www.hpinfotech.ro/SSD1309.pdf Pinout & Wiring: ---------------- OLED 2.42ยดยด SPI - SSD1309 UNO R3 Nano Mega 2560 Rev3 Sparkfun Pro Micro ------------------------- ---------- --------- -------------- ------------------ CS (chip select) 10 (SS) 10 53 10 DC (data/commands) 9 9 49 9 RES (reset) 7 7 48 7 SDA (serial data) 11 (MOSI) 11 (MOSI) 51 (COPI) 16 (MOSI) SCLK (clock) 13 (SCK) 13 (SCK) 52 (SCK) 15 (SCLK) VDD (supply voltage) +5 V +5V +5V +5V VSS (GND - ground) GND GND GND GND PeDa-Oscillator ---------------- Oscillator-Signal 8 8 ? 8 Vcc +5V +5V +5V +5V GND GND GND GND GND Neopixel-LEDs -------------- Data 3 3 ? ? Vcc +5V +5V +5V +5V GND GND GND GND GND Calibration-Button ------------------- 1st pin of button 5 5 2nd pin of button GND GND Wiring References: References for Arduino UNO: https://www.robotics.org.za/OLED242W-SPI https://forum.arduino.cc/t/2-42-oled-ssd1309-with-u8glib/425247/3 https://electropeak.com/learn/interfacing-2-42-inch-oled-spi-i2c-display-module-with-arduino/ References for Nano: ... the Nano uses the same wiring as the UNO does - you can refer to the UNO references References for Mega 2560: ... could not find a decent one ... Refs for Sparkfun Pro Micro: General good explanation: https://forum.arduino.cc/t/solved-ssd1309-with-spi-to-arduino-micro-help/1205438/4 Pinout References: Arduino UNO R3: https://docs.arduino.cc/hardware/uno-rev3/ (official site) https://www.circuito.io/blog/arduino-uno-pinout/ Arduino Nano: https://docs.arduino.cc/hardware/nano/ (official site) https://devboards.info/boards/arduino-nano Arduino Mega 2560: https://docs.arduino.cc/hardware/mega-2560/ (official site) https://devboards.info/boards/arduino-mega2560-rev3 Sparkfun Pro Micro: https://learn.sparkfun.com/tutorials/pro-micro--fio-v3-hookup-guide/hardware-overview-pro-micro (official site) https://learn.sparkfun.com/tutorials/pro-micro--fio-v3-hookup-guide SPI Pinout: for many Arduino types: https://www.arduino.cc/reference/en/language/functions/communication/spi/ Arduino-Terminology (old vs. new) Master/Slave (OLD) Controller/Peripheral (NEW) -------------------------- ------------------------------------ Master In Slave Out (MISO) Controller In, Peripheral Out (CIPO) Master Out Slave In (MOSI) Controller Out Peripheral In (COPI) Slave Select pin (SS) Chip Select Pin (CS) Reference: https://docs.arduino.cc/learn/communication/spi/ Libraries used: The program is using Oliver Kraus' graphic libraries (U8g2) for OLED-displays Reference: https://github.com/olikraus/u8g2 The program is also using Adafruits NeoPixel driver library: Reference: https://github.com/adafruit/Adafruit_NeoPixel */ #include #include // make sure, you import the U8g2 lib in Arduino IDE before using this line // To achieve this go to: Arduino IDE > Tools > Manage Libraries > Search for "U8g2" ... // ... > Install "U8g2 (by Oliver )" #include #include #include #include #include #include #include #include // make sure, you import the Adafruit lib in Arduino IDE before using this line // To achieve this go to: Arduino IDE > Tools > Manage Libraries > Search for "Neoxpixel" ... // ... > Install "Adafruit NeoxPixel (by Adafruit)" //#define DEBUG // uncomment this to show debug messages - not implemented yet / or still buggy #ifdef DEBUG #define D(x) x #else #define D(x) #endif // Set pinout int pinp = 8; // pin for puls input from oscillator int pout = 3; // pin for PWM output (just a reference-signal for testing) - must be a "pwm-capeable" pin. int pled = 4; // pin for LED output int pcal = 5; // pin to activate calibration mode (pcal = LOW) or normal operation mode (pcal = HIGH) #define PIN 3 // Which pin on the Arduino is connected to the NeoPixels? // Set some config-params int triggerlevelUnlocked = 20; // triggerlevel (%) - when LED shall be switched on/off and lock shall be considered unlocked/locked int triggerlevelLocked = 80; // triggerlevel (%) - when LED shall be switched on/off and lock shall be considered unlocked/locked int pwm = 6; // duty-cycle for the pwm test signal (pwm = 0 ... 255) int hysteresis = 1; // only pulselength that differ more than hysteresis from the previous pulselength are evaluated uint16_t noOfPulses = 200; // number of pulses to use for a measurement, i.e. number of // measurements averaged to form the actual measurement value // Do not choose to large values as sum of all pulseLength must fit in a uint16_t variable // i.e.: noOfPulses x pulseLenth < 65536 volatile uint16_t pulse_width; // volatile variable for ISR routine volatile bool isFinished; // volatile variable for ISR routine uint16_t maxTicks = 10000; // define max. no. of loops waiting for ISR to return, before sensor (oscillator) is regarded/detected as non-available bool noSensor = false; // assume by default, that sensor (oscillator) is available uint16_t pulseLengthMin = 536; // define minimum pulse length (effects/adjusts the left end of the progress bar) uint16_t pulseLengthMax = 665; // define maximum pulse length (effects/adjusts the right end of the progress bar) int direction = 0; // global variable used to mark the direction of adjustment in calibration mode // e.g. if pulseLengthMin is increased/decreased the direction variable is +1/-1. int16_t pulseLengthRange = pulseLengthMax - pulseLengthMin; // calculate width of range for pulse length int16_t pulseLengthUnlocked = pulseLengthMin + pulseLengthRange * triggerlevelUnlocked / 100; // calculate triggerlevel for "unlocked" detection int16_t pulseLengthLocked = pulseLengthMin + pulseLengthRange * triggerlevelLocked / 100; // calculate triggerlevel for "locked" detection // Define some bitmaps for Icons (e.g. with online editor https://xbm.jazzychad.net/) static const unsigned char image_Volup_8x6_bits[] U8X8_PROGMEM = { 0x48, 0x8c, 0xaf, 0xaf, 0x8c, 0x48 }; static const unsigned char image_Lock_8x8_bits[] U8X8_PROGMEM = { 0x1e, 0x12, 0x12, 0x3f, 0x3f, 0x33, 0x3f, 0x3f }; static const unsigned char image_Unlock_8x8_bits[] U8X8_PROGMEM = { 0xf0, 0x90, 0x90, 0x3f, 0x3f, 0x33, 0x3f, 0x3f }; static const unsigned char image_Bluetooth_Idle_5x8_bits[] U8X8_PROGMEM = { 0x04, 0x0d, 0x16, 0x0c, 0x0c, 0x16, 0x0d, 0x04 }; static const unsigned char image_Alert_9x8_bits[] U8X8_PROGMEM = { 0x10, 0x00, 0x38, 0x00, 0x28, 0x00, 0x6c, 0x00, 0x6c, 0x00, 0xfe, 0x00, 0xee, 0x00, 0xff, 0x01 }; // init some global variables uint16_t pl = 0; // pulse length uint16_t plOld = 0; // previous/old pulse length (measured in last measurement) bool calibrationNeeded = false; // set to true, if pulse length exceeds pulseLengthMin or pulseLengthMax int progress = 0; // progress bar = 0 (max. = 100) typedef enum { // type definition used throughout this program - defines different locks states UNLOCKED, INTERMEDIATE, LOCKED, UNKNOWN } State; State oldState = UNLOCKED; // set initial old state of the lock to UNLOCKED (meaning: lock is assumed to have been open at startup) State newState = UNLOCKED; // set initial state of the lock to UNLOCKED (meaning: lock is assumed to be open at startup) boolean inCalibration = false; // denotes, if program is in "calibration mode" (i.e. calibration button is pressed) char buffer[32]; // Constructor - for OLED display library - see u8g2 documentation U8G2_SSD1309_128X64_NONAME0_F_4W_HW_SPI u8g2(U8G2_R0, 10, 9, 7); #define NUMPIXELS 3 // Define, how many NeoPixels are attached to the Arduino ... // Define an enum to ease setting the neopixel-"traffic light" typedef enum { RED, YELLOW, GREEN, BLUE, WHITE, OFF } Colour; // When setting up the NeoPixel library, we tell it, how many pixels, // and which pin to use to send signals. Note that for older NeoPixel // strips you might need to change the third parameter -- see the // strandtest example for more information on possible values. Adafruit_NeoPixel pixels(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800); void setup() { pulseLengthMin = readUint16_tFromEEPROM(0); // read min value for pulselength from persistent eeprom pulseLengthMax = readUint16_tFromEEPROM(2); // read max value for pulselength from persistent eeprom // set some reasonable values if eeprom is empty (i.e. when you run the program for the very first time) if (pulseLengthMin == 0) { pulseLengthMin = 536; } if (pulseLengthMax == 0) { pulseLengthMax = 665; } pulseLengthRange = pulseLengthMax - pulseLengthMin; pulseLengthUnlocked = pulseLengthMin + pulseLengthRange * triggerlevelUnlocked / 100; pulseLengthLocked = pulseLengthMin + pulseLengthRange * triggerlevelLocked / 100; // set input and output pins pinMode(pinp, INPUT); // oszillator signal comes in here pinMode(pout, OUTPUT); pinMode(pled, OUTPUT); pinMode(pcal, INPUT); digitalWrite(pcal, HIGH); // turn on internal pullup for calibration button u8g2.begin(); // start the u8g2 library u8g2.setBitmapMode(1); //u8g2.setFont(u8g2_font_helvB08_tr); u8g2.setFont(u8g2_font_haxrcorp4089_tr); analogWrite(pout, 6); // just for testing: set up a 50% PWM at pout pin // Set input capture mode TCCR1A = 0; TCCR1B = (1 << ICNC1) | (1 << ICES1) | (1 << CS10); TIMSK1 |= (1 << ICIE1); pixels.begin(); // INITIALIZE NeoPixel strip object (REQUIRED) pixels.clear(); // Set all pixel colors to 'off' } void loop() { int16_t pulseLength; // clear display buffer u8g2.clearBuffer(); // measure pulse length pulseLength = measurePulseLength(); // Hysteresis: change pulseLength only if its change exceeds hysteresis if (abs((int)pulseLength - (int)plOld) > hysteresis) { plOld = pulseLength; } else { pulseLength = plOld; } // Map pulseLength to percentage scale (0 ... 100) progress = ((int)pulseLength - (int)pulseLengthMin) * 100 / pulseLengthRange; // Check if pulseLength falls short/exceeds pulseLengthMin/pulseLengthMax => set calibrationNeeded flag if (pulseLength > pulseLengthMax) { calibrationNeeded = true; } if (pulseLength < pulseLengthMin) { calibrationNeeded = true; } // check for lock state changes (UNLOCKED/INTERMEDIATE/LOCKED) - if a state change happens => oldState != newState if (pulseLength > pulseLengthLocked) { oldState = newState; newState = LOCKED; } else if (pulseLength < pulseLengthUnlocked) { oldState = newState; newState = UNLOCKED; } else { oldState = newState; newState = INTERMEDIATE; } // change the illumination of the NewPixel-"traffic light" only after a state change has happened if (oldState != newState && !calibrationNeeded) { if (newState == LOCKED) { // pixels.Color() takes RGB values, from 0,0,0 up to 255,255,255 driveNeoPixels(OFF, OFF, RED); } if (newState == UNLOCKED) { driveNeoPixels(GREEN, OFF, OFF); } if (newState == INTERMEDIATE) { driveNeoPixels(OFF, YELLOW, OFF); } } if (calibrationNeeded || noSensor) { driveNeoPixels(RED, RED, RED); } // Calibration routine // if calibration button is pressed => the min or max bargraph level is adjusted to the actual value if (digitalRead(pcal) == 0) { inCalibration = true; if (pulseLength < pulseLengthMin) { // pulseLength < min pulseLengthMin--; direction = -1; // "direction = -1" => shift min-bound left } else if (pulseLength > pulseLengthMax) { // pulseLength > max pulseLengthMax++; direction = 1; // "direction = +1" => shift max-bound right } else if ((pulseLength == pulseLengthMin) || (pulseLength == pulseLengthMax)) { // pulseLength = min|max driveNeoPixels(OFF, OFF, OFF); // calibration done => switch off LEDs } else { // min < pulseLength < max if (((pulseLength - pulseLengthMin) <= (pulseLengthMax - pulseLength)) && (direction >= 0)) { // pulseLength close to min => shift min-bound right pulseLengthMin++; direction = 1; } else if (((pulseLength - pulseLengthMin) > (pulseLengthMax - pulseLength)) && (direction <= 0)) { // pulseLength close to max => shift max-bound left pulseLengthMax--; direction = -1; } } pulseLengthRange = pulseLengthMax - pulseLengthMin; // re-calculate range of pulseLength pulseLengthUnlocked = pulseLengthMin + pulseLengthRange * triggerlevelUnlocked / 100; // re-calculate trigger level for unlock-detection pulseLengthLocked = pulseLengthMin + pulseLengthRange * triggerlevelLocked / 100; // re-calculate trigger level for locked-detection // print min/max-values of the pulseLengthMin and pulseLengthMax sprintf(buffer, "%u", pulseLengthMin); u8g2.drawStr(5, 20, buffer); sprintf(buffer, "KALIBRIERUNG %u", pulseLengthMax); u8g2.drawStr(35, 20, buffer); // unset flag calibrationNeeded = false; } else { // write min/max values to eeprom if calibration button is released (only once) if (inCalibration == true) { writeUint16_tIntoEEPROM(0, pulseLengthMin); writeUint16_tIntoEEPROM(2, pulseLengthMax); newState = UNKNOWN; } inCalibration = false; direction = 0; } // draw outer frame for progress bar u8g2.drawFrame(11, 21, 104, 17); // Show "calibration needed or sensor broken" warning if corresponding flags are set if (calibrationNeeded && !noSensor) { sprintf(buffer, "Ggf. Kalibrieren ..."); u8g2.drawStr(28, 20, buffer); sprintf(buffer, "... oder Sensor defekt ..."); u8g2.drawStr(20, 62, buffer); } // draw progress bar if (progress >= 0) { u8g2.drawBox(13, 23, progress, 13); } else { u8g2.drawBox(13 + progress, 23, -progress, 13); } // draw triggerlevel-Lines that demarc the unlocked/indifferent/locked state u8g2.setDrawColor(0); u8g2.setBitmapMode(0); u8g2.drawLine(13 + triggerlevelUnlocked, 19, 13 + triggerlevelUnlocked, 40); u8g2.drawLine(13 + triggerlevelLocked, 19, 13 + triggerlevelLocked, 40); u8g2.setDrawColor(1); u8g2.setBitmapMode(1); // print %-value of progress bar sprintf(buffer, "%d%%", progress); u8g2.drawStr(103, 8, buffer); // debug info (not used yet) D(sprintf(buffer, "Pulse: %u (1Pulse = 1/16us)", pulseLength); u8g2.drawStr(0, 64, buffer);) // draw icons u8g2.drawStr(1, 8, "Riegel-Sensor"); //sprintf(buffer, "Trigger: %u %u state: %d", pulseLengthUnlocked, pulseLengthLocked, (int)newState); //u8g2.drawStr(1, 8, buffer); u8g2.drawLine(1, 11, 126, 11); //u8g2.drawXBMP(117, 2, 8, 6, image_Volup_8x6_bits); // draw icons symbolizing UNLOCKED (=oper lock icon) / INTERMEDIATE (=alert icon) / LOCKED (=closed lock icon) if (!inCalibration && !noSensor && !calibrationNeeded) { if (newState == LOCKED) { u8g2.drawXBMP(103, 42, 8, 8, image_Lock_8x8_bits); } else if (newState == UNLOCKED) { u8g2.drawXBMP(18, 42, 8, 8, image_Unlock_8x8_bits); } else { u8g2.drawXBMP(60, 42, 9, 8, image_Alert_9x8_bits); } } // draw lockstate-icons when in "normal" mode if ((!inCalibration) && (!noSensor) && (!calibrationNeeded)) { if (newState == LOCKED) { sprintf(buffer, "Verriegelt"); u8g2.drawStr(80, 62, buffer); } else if (newState == UNLOCKED) { sprintf(buffer, "Entriegelt"); u8g2.drawStr(10, 62, buffer); } else { sprintf(buffer, "Achtung: Riegel haengt!"); u8g2.drawStr(13, 62, buffer); } } // draw alert-icons when no sensor was detected or when calibration is needed if (noSensor || calibrationNeeded) { u8g2.drawXBMP(18, 42, 9, 8, image_Alert_9x8_bits); u8g2.drawXBMP(60, 42, 9, 8, image_Alert_9x8_bits); u8g2.drawXBMP(103, 42, 9, 8, image_Alert_9x8_bits); } // print additional info when no sensor was detected if (noSensor) { sprintf(buffer, "? Sensor defekt ?"); u8g2.drawStr(29, 33, buffer); sprintf(buffer, "... kein Sensor erkannt ..."); u8g2.drawStr(16, 62, buffer); } // send the whole buffer to display u8g2.sendBuffer(); delay(100); } // function to measure the pulse length (uses the ISR(TIMER1_CAPT_vect) values) uint16_t measurePulseLength() { uint16_t ticks = 0; uint32_t sum = 0; uint16_t pulseLength; noSensor = false; // repeat measurement noOfPulses times to average the pulseLength for (uint16_t i = 0; i < noOfPulses; i++) { isFinished = false; // isFinished is set to true by ISR-routine when measurement is finished (= at falling edge) ticks = 0; // counter to detect missing pulse signal while (!isFinished) { // isFinished stays false until a new measurement is finished ticks++; if (ticks > maxTicks) { // if ticks > maxTicks, we have been waiting to long for a new measurement => pulse signal is missing noSensor = true; return (pulseLengthMin); } } ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { // copy ISR-variable (pulse_width) to local variable (pulseLength) - must be ATOMIC operation!! pulseLength = pulse_width; } sum += pulseLength; } return sum / noOfPulses; // return the average pulse length (averaged over noOfPulses) } // ISR to capture time for rising/falling edge (calculates also pulse length) ISR(TIMER1_CAPT_vect) { static uint16_t tr; uint16_t tf; if (TCCR1B & (1 << ICES1)) { // ISR was triggered by rising edge tr = ICR1; // tr: time of rising edge TCCR1B = (1 << ICNC1) | (1 << CS10); // next trigger on falling edge } else { // ISR was triggered by falling edge tf = ICR1; // tf: time of falling edge TCCR1B = (1 << ICNC1) | (1 << ICES1) | (1 << CS10); // set next trigger to happen on rising edge pulse_width = tf - tr; // calculate pulse_width isFinished = true; // flag marks end of pulse measurement } TIFR1 = (1 << ICF1); // just to fullfill datasheet requirement (required when edge detection is changed (rising <-> falling) ) } // see: https://roboticsbackend.com/arduino-store-int-into-eeprom/ void writeUint16_tIntoEEPROM(int address, uint16_t number) { EEPROM.write(address, number >> 8); EEPROM.write(address + 1, number & 0xFF); } uint16_t readUint16_tFromEEPROM(int address) { return (uint16_t)((EEPROM.read(address) << 8) + EEPROM.read(address + 1)); } // drive the "traffic light" (made out of 3 NeoPixels): set the colour of each light. void driveNeoPixels(Colour c0, Colour c1, Colour c2) { Colour colour; // Brightness of LED's for each colour can be defined here uint8_t rBreight = 10; // intensity of red led in red colour uint8_t gBreight = 10; // intensity of green led in green colour uint8_t bBreight = 10; // intensity of glue led in blue colour uint8_t yrBreight = 13; // intensity of red led in yellow colour uint8_t ygBreight = 11; // intensity of green led in yellow colour // cycle through the 3 lights and set each light according to the values of (c1, c2, c3) for (uint8_t i=0; i<3; i++) { switch (i) { case 0: colour = c0; break; case 1: colour = c1; break; case 2: colour = c2; break; } switch (colour) { case RED: pixels.setPixelColor(i, pixels.Color(rBreight, 0, 0)); break; case YELLOW: pixels.setPixelColor(i, pixels.Color(yrBreight, ygBreight, 0)); break; case GREEN: pixels.setPixelColor(i, pixels.Color(0, gBreight, 0)); break; case BLUE: pixels.setPixelColor(i, pixels.Color(0, 0, bBreight)); break; case OFF: pixels.setPixelColor(i, pixels.Color(0, 0, 0)); break; case WHITE: pixels.setPixelColor(i, pixels.Color(rBreight, gBreight, bBreight)); break; default: pixels.setPixelColor(i, pixels.Color(0, 0, 0)); break; } } pixels.show(); // Send the updated pixel colors to the hardware. return; }