diff --git a/IO_HCSR04.h b/IO_HCSR04.h index e7b5622..26f0ecd 100644 --- a/IO_HCSR04.h +++ b/IO_HCSR04.h @@ -30,7 +30,7 @@ * * This driver polls the HC-SR04 by sending the trigger pulse and then measuring * the length of the received pulse. If the calculated distance is less than - * the threshold, the output state returned by a read() call changes to 1. If + * the threshold, the output _state returned by a read() call changes to 1. If * 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 @@ -48,6 +48,20 @@ * 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. + * + * 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); + * */ #ifndef IO_HCSR04_H @@ -61,38 +75,52 @@ private: // pins must be arduino GPIO pins, not extender pins or HAL pins. int _trigPin = -1; int _echoPin = -1; - // Thresholds for setting active state in cm. + // Thresholds for setting active _state in cm. uint8_t _onThreshold; // cm uint8_t _offThreshold; // cm // Last measured distance in cm. uint16_t _distance; - // Active=1/inactive=0 state + // Active=1/inactive=0 _state uint8_t _value = 0; - // Factor for calculating the distance (cm) from echo time (ms). + // Factor for calculating the distance (cm) from echo time (us). // Based on a speed of sound of 345 metres/second. - const uint16_t factor = 58; // ms/cm + 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; public: + enum Options { + LOOP = 1, // Option HCSR04::LOOP reinstates old behaviour, i.e. complete measurement in one loop entry. + }; // Static create function provides alternative way to create object - static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { + static void create(VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options = 0) { if (checkNoOverlap(vpin)) - new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold); + new HCSR04(vpin, trigPin, echoPin, onThreshold, offThreshold, options); } protected: - // Constructor perfroms static initialisation of the device object - HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold) { + // Constructor performs static initialisation of the device object + HCSR04 (VPIN vpin, int trigPin, int echoPin, uint16_t onThreshold, uint16_t offThreshold, uint16_t options) { _firstVpin = vpin; _nPins = 1; _trigPin = trigPin; _echoPin = echoPin; _onThreshold = onThreshold; _offThreshold = offThreshold; + _options = options; addDevice(this); } // _begin function called to perform dynamic initialisation of the device void _begin() override { + _state = 0; pinMode(_trigPin, OUTPUT); pinMode(_echoPin, INPUT); ArduinoPins::fastWriteDigital(_trigPin, 0); @@ -112,11 +140,97 @@ protected: return _distance; } - // _loop function - read HC-SR04 once every 50 milliseconds. + // _loop function - read HC-SR04 once every 100 milliseconds. void _loop(unsigned long currentMicros) override { - read_HCSR04device(); - // Delay next loop entry until 50ms have elapsed. - delayUntil(currentMicros + 50000UL); + 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 + } void _display() override { @@ -124,66 +238,6 @@ protected: _firstVpin, _trigPin, _echoPin, _onThreshold, _offThreshold); } -private: - // This polls the HC-SR04 device by sending a pulse and measuring the duration of - // the pulse observed on the receive pin. In order to be kind to the rest of the CS - // software, no interrupts are used and interrupts are not disabled. The pulse duration - // is measured in a loop, using the micros() function. Therefore, interrupts from other - // sources may affect the result. However, interrupts response code in CS typically takes - // much less than the 58us frequency for the DCC interrupt, and 58us corresponds to only 1cm - // in the HC-SR04. - // To reduce chatter on the output, hysteresis is applied on reset: the output is set to 1 when the - // measured distance is less than the onThreshold, and is set to 0 if the measured distance is - // greater than the offThreshold. - // - void read_HCSR04device() { - // uint16 enough to time up to 65ms - uint16_t startTime, waitTime = 0, currentTime, maxTime; - - // If receive pin is still set on from previous call, abort the read. - if (ArduinoPins::fastReadDigital(_echoPin)) - return; - - // Send 10us pulse to trigger transmitter - ArduinoPins::fastWriteDigital(_trigPin, 1); - delayMicroseconds(10); - ArduinoPins::fastWriteDigital(_trigPin, 0); - - // Wait for receive pin to be set - startTime = currentTime = micros(); - maxTime = factor * _offThreshold * 2; - while (!ArduinoPins::fastReadDigital(_echoPin)) { - // lastTime = currentTime; - currentTime = micros(); - waitTime = currentTime - startTime; - if (waitTime > maxTime) { - // Timeout waiting for pulse start, abort the read - return; - } - } - - // Wait for receive pin to reset, and measure length of pulse - startTime = currentTime = micros(); - maxTime = factor * _offThreshold; - while (ArduinoPins::fastReadDigital(_echoPin)) { - currentTime = micros(); - waitTime = currentTime - startTime; - // 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, reset value. - _value = 0; - _distance = 32767; - return; - } - } - // Check if pulse length is below threshold, if so set value. - //DIAG(F("HCSR04: Pulse Len=%l Distance=%d"), waitTime, distance); - _distance = waitTime / factor; // in centimetres - if (_distance < _onThreshold) - _value = 1; - } - }; #endif //IO_HCSR04_H