2021-08-18 00:41:34 +02:00
|
|
|
/*
|
|
|
|
* © 2021, Neil McKechnie. All rights reserved.
|
|
|
|
*
|
|
|
|
* This file is part of DCC++EX API
|
|
|
|
*
|
|
|
|
* This is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* It is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with CommandStation. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2021-09-15 01:23:24 +02:00
|
|
|
/*
|
2021-08-18 00:41:34 +02:00
|
|
|
* The HC-SR04 module has an ultrasonic transmitter (40kHz) and a receiver.
|
2021-09-15 01:23:24 +02:00
|
|
|
* It is operated through two signal pins. When the transmit pin is set to 1
|
|
|
|
* for 10us, on the falling edge the transmitter sends a short transmission of
|
2021-08-18 00:41:34 +02:00
|
|
|
* 8 pulses (like a sonar 'ping'). This is reflected off objects and received
|
|
|
|
* by the receiver. A pulse is sent on the receive pin whose length is equal
|
|
|
|
* to the delay between the transmission of the pulse and the detection of
|
|
|
|
* its echo. The distance of the reflecting object is calculated by halving
|
|
|
|
* the time (to allow for the out and back distance), then multiplying by the
|
|
|
|
* speed of sound (assumed to be constant).
|
2021-09-15 01:23:24 +02:00
|
|
|
*
|
2021-08-18 00:41:34 +02:00
|
|
|
* This driver polls the HC-SR04 by sending the trigger pulse and then measuring
|
2021-09-15 01:23:24 +02:00
|
|
|
* the length of the received pulse. If the calculated distance is less than
|
2023-03-17 22:15:26 +01:00
|
|
|
* the threshold, the output _state returned by a read() call changes to 1. If
|
2021-09-15 01:23:24 +02:00
|
|
|
* the distance is greater than the threshold plus a hysteresis margin, the
|
|
|
|
* output changes to 0. The device also supports readAnalogue(), which returns
|
|
|
|
* the measured distance in cm, or 32767 if the distance exceeds the
|
|
|
|
* offThreshold.
|
|
|
|
*
|
|
|
|
* It might be thought that the measurement would be more reliable if interrupts
|
|
|
|
* were disabled while the pulse is being timed. However, this would affect
|
|
|
|
* other functions in the CS so the measurement is being performed with
|
|
|
|
* interrupts enabled. Also, we could use an interrupt pin in the Arduino for
|
|
|
|
* the timing, but the same consideration applies. In any case, the DCC
|
|
|
|
* interrupt occurs once every 58us, so any IRC code is much faster than that.
|
|
|
|
* And 58us corresponds to 1cm in the calculation, so the effect of
|
|
|
|
* interrupts is negligible.
|
|
|
|
*
|
|
|
|
* Note: The timing accuracy required for measuring the pulse length means that
|
|
|
|
* the pins have to be direct Arduino pins; GPIO pins on an IO Extender cannot
|
|
|
|
* provide the required accuracy.
|
2023-03-17 22:15:26 +01:00
|
|
|
*
|
|
|
|
* Example configuration:
|
|
|
|
* HCSR04::create(23000, 32, 33, 80, 85);
|
|
|
|
*
|
|
|
|
* Where 23000 is the VPIN allocated,
|
|
|
|
* 32 is the pin connected to the HCSR04 trigger terminal,
|
|
|
|
* 33 is the pin connected to the HCSR04 echo terminal,
|
|
|
|
* 80 is the distance in cm below which pin 23000 will be active,
|
|
|
|
* and 85 is the distance in cm above which pin 23000 will be inactive.
|
|
|
|
*
|
|
|
|
* Alternative configuration, which hogs the processor until the measurement is complete
|
|
|
|
* (old behaviour, more accurate but higher impact on other CS tasks):
|
|
|
|
* HCSR04::create(23000, 32, 33, 80, 85, HCSR04::LOOP);
|
|
|
|
*
|
2021-08-18 00:41:34 +02:00
|
|
|
*/
|
|
|
|
|
|
|
|
#ifndef IO_HCSR04_H
|
|
|
|
#define IO_HCSR04_H
|
|
|
|
|
|
|
|
#include "IODevice.h"
|
|
|
|
|
|
|
|
class HCSR04 : public IODevice {
|
|
|
|
|
|
|
|
private:
|
|
|
|
// pins must be arduino GPIO pins, not extender pins or HAL pins.
|
2021-09-16 01:17:26 +02:00
|
|
|
int _trigPin = -1;
|
|
|
|
int _echoPin = -1;
|
2023-03-17 22:15:26 +01:00
|
|
|
// Thresholds for setting active _state in cm.
|
2021-08-18 00:41:34 +02:00
|
|
|
uint8_t _onThreshold; // cm
|
|
|
|
uint8_t _offThreshold; // cm
|
2021-09-15 01:23:24 +02:00
|
|
|
// Last measured distance in cm.
|
|
|
|
uint16_t _distance;
|
2023-03-17 22:15:26 +01:00
|
|
|
// Active=1/inactive=0 _state
|
2021-08-18 00:41:34 +02:00
|
|
|
uint8_t _value = 0;
|
2023-03-17 22:15:26 +01:00
|
|
|
// Factor for calculating the distance (cm) from echo time (us).
|
2021-08-18 00:41:34 +02:00
|
|
|
// Based on a speed of sound of 345 metres/second.
|
2023-03-17 22:15:26 +01:00
|
|
|
const uint16_t factor = 58; // us/cm
|
|
|
|
// Limit the time spent looping by dropping out when the expected
|
|
|
|
// worst case threshold value is greater than an arbitrary value.
|
|
|
|
const uint16_t maxPermittedLoopTime = 10 * factor; // max in us
|
|
|
|
unsigned long _startTime = 0;
|
|
|
|
unsigned long _maxTime = 0;
|
|
|
|
enum {DORMANT, MEASURING}; // _state values
|
|
|
|
uint8_t _state = DORMANT;
|
|
|
|
uint8_t _counter = 0;
|
|
|
|
uint16_t _options = 0;
|
2021-08-18 00:41:34 +02:00
|
|
|
|
|
|
|
public:
|
2023-03-17 22:15:26 +01:00
|
|
|
enum Options {
|
|
|
|
LOOP = 1, // Option HCSR04::LOOP reinstates old behaviour, i.e. complete measurement in one loop entry.
|
|
|
|
};
|
2022-06-14 16:23:27 +02:00
|
|
|
|
|
|
|
// Static create function provides alternative way to create object
|
2023-03-17 22:15:26 +01:00
|
|
|
static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options = 0) {
|
2022-06-14 18:50:57 +02:00
|
|
|
if (checkNoOverlap(vpin))
|
2023-03-17 22:15:26 +01:00
|
|
|
new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold, options);
|
2022-06-14 16:23:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
protected:
|
2023-03-17 22:15:26 +01:00
|
|
|
// Constructor performs static initialisation of the device object
|
|
|
|
HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options) {
|
2021-08-18 00:41:34 +02:00
|
|
|
_firstVpin = vpin;
|
|
|
|
_nPins = 1;
|
2021-09-16 01:17:26 +02:00
|
|
|
_trigPin = trigPin;
|
|
|
|
_echoPin = echoPin;
|
2021-08-18 00:41:34 +02:00
|
|
|
_onThreshold = onThreshold;
|
|
|
|
_offThreshold = offThreshold;
|
2023-03-17 22:15:26 +01:00
|
|
|
_options = options;
|
2021-08-18 00:41:34 +02:00
|
|
|
addDevice(this);
|
|
|
|
}
|
2022-06-14 16:23:27 +02:00
|
|
|
// _begin function called to perform dynamic initialisation of the device
|
2021-08-18 00:41:34 +02:00
|
|
|
void _begin() override {
|
2023-03-17 22:15:26 +01:00
|
|
|
_state = 0;
|
2021-09-16 01:17:26 +02:00
|
|
|
pinMode(_trigPin, OUTPUT);
|
|
|
|
pinMode(_echoPin, INPUT);
|
|
|
|
ArduinoPins::fastWriteDigital(_trigPin, 0);
|
2021-08-27 16:44:26 +02:00
|
|
|
#if defined(DIAG_IO)
|
|
|
|
_display();
|
|
|
|
#endif
|
2021-08-18 00:41:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// _read function - just return _value (calculated in _loop).
|
|
|
|
int _read(VPIN vpin) override {
|
|
|
|
(void)vpin; // avoid compiler warning
|
|
|
|
return _value;
|
|
|
|
}
|
|
|
|
|
2021-09-15 01:23:24 +02:00
|
|
|
int _readAnalogue(VPIN vpin) override {
|
|
|
|
(void)vpin; // avoid compiler warning
|
|
|
|
return _distance;
|
|
|
|
}
|
|
|
|
|
2023-03-17 22:15:26 +01:00
|
|
|
// _loop function - read HC-SR04 once every 100 milliseconds.
|
2021-08-18 00:41:34 +02:00
|
|
|
void _loop(unsigned long currentMicros) override {
|
2023-03-17 22:15:26 +01:00
|
|
|
unsigned long waitTime;
|
|
|
|
switch(_state) {
|
|
|
|
case DORMANT: // Issue pulse
|
|
|
|
// If receive pin is still set on from previous call, do nothing till next entry.
|
|
|
|
if (ArduinoPins::fastReadDigital(_echoPin)) return;
|
|
|
|
|
|
|
|
// Send 10us pulse to trigger transmitter
|
|
|
|
ArduinoPins::fastWriteDigital(_trigPin, 1);
|
|
|
|
delayMicroseconds(10);
|
|
|
|
ArduinoPins::fastWriteDigital(_trigPin, 0);
|
|
|
|
|
|
|
|
// Wait, with timeout, for echo pin to become set.
|
|
|
|
// Measured time delay is just under 500us, so
|
|
|
|
// wait for max of 1000us.
|
|
|
|
_startTime = micros();
|
|
|
|
_maxTime = 1000;
|
|
|
|
|
|
|
|
while (!ArduinoPins::fastReadDigital(_echoPin)) {
|
|
|
|
// Not set yet, see if we've timed out.
|
|
|
|
waitTime = micros() - _startTime;
|
|
|
|
if (waitTime > _maxTime) {
|
|
|
|
// Timeout waiting for pulse start, abort the read and start again
|
|
|
|
_state = DORMANT;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Echo pulse started, so wait for echo pin to reset, and measure length of pulse
|
|
|
|
_startTime = micros();
|
|
|
|
_maxTime = factor * _offThreshold;
|
|
|
|
_state = MEASURING;
|
|
|
|
// If maximum measurement time is high, then skip until next loop entry before
|
|
|
|
// starting to look for pulse end.
|
|
|
|
// This gives better accuracy at shorter distance thresholds but without extending
|
|
|
|
// loop execution time for longer thresholds. If LOOP option is set on, then
|
|
|
|
// the entire measurement will be done in one loop entry, i.e. the code will fall
|
|
|
|
// through into the measuring phase.
|
|
|
|
if (!(_options & LOOP) && _maxTime > maxPermittedLoopTime) break;
|
|
|
|
/* fallthrough */
|
|
|
|
|
|
|
|
case MEASURING: // Check if echo pulse has finished
|
|
|
|
do {
|
|
|
|
waitTime = micros() - _startTime;
|
|
|
|
if (!ArduinoPins::fastReadDigital(_echoPin)) {
|
|
|
|
// Echo pulse completed; check if pulse length is below threshold and if so set value.
|
|
|
|
if (waitTime <= factor * _onThreshold) {
|
|
|
|
// Measured time is within the onThreshold, so value is one.
|
|
|
|
_value = 1;
|
|
|
|
// If the new distance value is less than the current, use it immediately.
|
|
|
|
// But if the new distance value is longer, then it may be erroneously long
|
|
|
|
// (because of extended loop times delays), so apply a delay to distance increases.
|
|
|
|
uint16_t estimatedDistance = waitTime / factor;
|
|
|
|
if (estimatedDistance < _distance)
|
|
|
|
_distance = estimatedDistance;
|
|
|
|
else
|
|
|
|
_distance += 1; // Just increase distance slowly.
|
|
|
|
_counter = 0;
|
|
|
|
//DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, _distance);
|
|
|
|
}
|
|
|
|
_state = DORMANT;
|
|
|
|
} else {
|
|
|
|
// Echo pulse hasn't finished, so check if maximum time has elapsed
|
|
|
|
// If pulse is too long then set return value to zero,
|
|
|
|
// and finish without waiting for end of pulse.
|
|
|
|
if (waitTime > _maxTime) {
|
|
|
|
// Pulse length longer than maxTime, value is provisionally zero.
|
|
|
|
// But don't change _value unless provisional value is zero for 10 consecutive measurements
|
|
|
|
if (_value == 1) {
|
|
|
|
if (++_counter >= 10) {
|
|
|
|
_value = 0;
|
|
|
|
_distance = 32767;
|
|
|
|
_counter = 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_state = DORMANT; // start again
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If there's lots of time remaining before the expected completion time,
|
|
|
|
// then exit and wait for next loop entry. Otherwise, loop until we finish.
|
|
|
|
// If option LOOP is set, then we loop until finished anyway.
|
|
|
|
uint32_t remainingTime = _maxTime - waitTime;
|
|
|
|
if (!(_options & LOOP) && remainingTime < maxPermittedLoopTime) return;
|
|
|
|
} while (_state == MEASURING) ;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
// Datasheet recommends a wait of at least 60ms between measurement cycles
|
|
|
|
if (_state == DORMANT)
|
|
|
|
delayUntil(currentMicros+60000UL); // wait 60ms till next measurement
|
|
|
|
|
2021-08-18 00:41:34 +02:00
|
|
|
}
|
|
|
|
|
2021-08-27 16:44:26 +02:00
|
|
|
void _display() override {
|
2023-03-27 14:08:14 +02:00
|
|
|
DIAG(F("HCSR04 Configured on VPIN:%u TrigPin:%d EchoPin:%d On:%dcm Off:%dcm"),
|
2021-09-16 01:17:26 +02:00
|
|
|
_firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold);
|
2021-08-27 16:44:26 +02:00
|
|
|
}
|
|
|
|
|
2021-08-18 00:41:34 +02:00
|
|
|
};
|
|
|
|
|
2022-10-20 00:53:05 +02:00
|
|
|
#endif //IO_HCSR04_H
|