Fail to control (Ultrasonic sensor) HC-SR04 using port registers

I am writing my own library to control a little robot.
This library should make it possible letting students at our primary school to program the robot in a more kids friendly way. So all the complexity of controlling stepper motors, servo's and sensors is 'hidden' in the library.

After successfully creating a library to contol the stepper motors using port register i ran into an issue controlling the HC-SR04. The Trigger and Echo pin to the sensor are connected to the same port. After triggering the sensor, the port mode is changed to input mode and the pulse duration is measured. When using the Arduino library, the code works fine. Currently i have replaced the code using the Arduino libraries triggering the HC-SR04 using port registers and that works fine.

#include "LandjePingSensor.h"

LandjePingSensor::Init(int pingSensorPort)
{

    _pingSensorPort = pingSensorPort;
    _distanceInCM = 0;

    _pingPinBitmask = digitalPinToBitMask(_pingSensorPort);  // translate pin number to port register bitmask for selected pin.
    _pingOutputRegister = portOutputRegister(digitalPinToPort(_pingSensorPort)); // Get the output port register for selected pin.
    _echoInputRegister = portInputRegister(digitalPinToPort(_pingSensorPort)); // Get the input port register for the echo pin.
    _pingModeRegister = (uint8_t *) portModeRegister(digitalPinToPort(_pingSensorPort)) ; // get DDR register for pin
};

LandjePingSensor::Ping()
{

    *_pingModeRegister |= _pingPinBitmask; // Set pin to output in DDR register
    *_pingOutputRegister &= ~_pingPinBitmask; // LOW
    delayMicroseconds(2);
    *_pingOutputRegister |= _pingPinBitmask; // HIGH
    delayMicroseconds(10);
    *_pingOutputRegister &= ~_pingPinBitmask; // LOW
    *_pingModeRegister &= ~_pingPinBitmask; // Set pin to input in DDR register

    int duration = pulseIn(_pingSensorPort, HIGH);
    _distanceInCM = duration/58.2;
}

int LandjePingSensor::GetDistanceInCM()
{
    return (_distanceInCM);
}

When initializing calling the functions, it does return the correct distance;

[code]LandjePingSensor p;
p.Init(pingSensor) ;
p.Ping() ;
Serial.println(p.GetDistanceInCM()) ;

To replace the pulseIn statement (int duration = pulseIn(_pingSensorPort, HIGH); _distanceInCM = duration/58.2;) i wrote the following code

      Serial.println("wait for previous");
    // if pin on inputregister is HIGH echo is still receiving, probably halfway other ping
    if (!(*_echoInputRegister & _pingPinBitmask))
    {
        Serial.println("done waiting for previous");
        long _startPing = 0;
        Serial.println(_startPing);
        long _duration = 0;
        Serial.println("done waiting echo");
        // wait for the pulse to become HIGH
        // micros + max echo time (25cm * 58.2) 
        long maxwaitdur = micros() +  (25*58.2)  ;
        boolean waittimeout = false;

        // Wait till pin becomes high
        while ((!(*_echoInputRegister & _pingPinBitmask)) & !waittimeout)
    
        { //
            Serial.println(*_echoInputRegister) ;
            Serial.println(_pingPinBitmask) ;
            Serial.println("While 1");
            Serial.println(micros());
            Serial.println(maxwaitdur);
            
            if (micros() > maxwaitdur)
            {
                waittimeout = true;
                Serial.println("Timeout 1");
            }
        }

        Serial.println("Pin raised");

        // if no timeout occured, wait for ping to become liw
        if (!waittimeout)
        {
            _startPing = micros();
            maxwaitdur = micros() + 1000;
            // wait till pin becomes low (echo received)
            while ((*_echoInputRegister & _pingPinBitmask) & !waittimeout)
            {
                if ((micros() > maxwaitdur))
                {
                    waittimeout = true;
                    Serial.println("Timeout 2");
                }
            }

            Serial.println("Pin lowered");
        }

        if (!waittimeout)
        {
            _duration = micros() - _startPing;
            Serial.println(_duration);
        }

        // wait for the echo

        Serial.println("done waiting for echo");

The code is still messy a bit messy, sorry for that. But the functionally is should do;

  • Skip the time duration measurement if Input at pin is high. Probably another echo is still being processed
  • Wait till the signal at the ping goes high
  • After the signal goes high, wait till it goes low
  • Measure the pulse duration

When running the code the output is;

wait for previous
done waiting for previous
0
done waiting echo
0
32
While 1
80376724
80381256
0
32
While 1
80382840
80381256
Timeout 1
Pin raised
done waiting for echo
0

For some not known reason to me is that the raised signal at the input in is not detected.
The value of the inout register is also 0 and therefore not matching the bitmask (32) of the port.

Any ideas ?

If you want to explore the whole class in detail, it can be found here ;
https://github.com/reddipped/LandjeRobot

Fixed it;

Header file

#ifndef LandjePingSensor_h
#define LandjePingSensor_h

#include "Arduino.h"
class LandjePingSensor
{

public:
  //  LandjePingSensor();
  Init(int pingSensorPort);

  Ping();

  int GetDistanceInCM();

private:
  const unsigned long _maxUnsignedLong = 1 ; //pow(2,32) - 1 ; // max value unsigned long
  const unsigned int _maxPingDistanceCM = 400; //maximum distance detected by ping sensor in cm
  const unsigned int _minPingDistanceCM = 2 ; // minimum detectable distance
  const unsigned long _maxSampleLifespan = 20000; // maximum lifespan of measurement in milliseconds 

  // The approximate speed of sound in dry air is given by the formula:
  // c = 331.5 (m/s) + 0.6 * [air temperature in degrees Celsius]
  // Give the temperatue of 21 Celcius;
  // 331.5 + (0.6 * 21) = 344.1 meter/sec = 34410 cm/sec
  // 34410 cm/sec = (34410 / 1000) cm/msec = 0.034410 cm/microsec
  const unsigned long _speedSoundCmPerUsec = 23 ;

  unsigned int _maxEchoWaitTime; // calculated max wait time in microseconds
  unsigned long _echoWaitTime; // timestamp when ping echo response measurement ends
  unsigned long _pingStartTime ; // stores start of ping echo reception
  unsigned long _sampleTime ; // timestamp of latest sample

  int _distanceInCM;
  int _pingSensorPort;

  uint8_t _pingPinBitmask;
  
    volatile uint8_t *_pingOutputRegister;
  
    volatile uint8_t *_echoInputRegister;
    volatile uint8_t *_pingModeRegister;
};

#endif

Class

#include "LandjePingSensor.h"
LandjePingSensor::Init(int pingSensorPort)
{
    _pingSensorPort = pingSensorPort;
    _distanceInCM = 0;
    _pingPinBitmask = digitalPinToBitMask(_pingSensorPort);                             // translate pin number to port register bitmask for selected pin.
    _pingOutputRegister = portOutputRegister(digitalPinToPort(_pingSensorPort));        // Get the output (PORT) port register for selected pin.
    _echoInputRegister = portInputRegister(digitalPinToPort(_pingSensorPort));          // Get the input (PIN) port register for the echo pin.
    _pingModeRegister = (uint8_t *)portModeRegister(digitalPinToPort(_pingSensorPort)); // get DDR register for pin

    // Sound has to travel to object and echoed back from the object.
    // The maximum time to wait for an echo is roughly the maximum distance detection
    // multiplied by 2 and divided by the traveling speed of sound in microseconds per cm.
    _maxEchoWaitTime = (long)((_maxPingDistanceCM * 20) * _speedSoundCmPerUsec);


};

LandjePingSensor::Ping()
{

    // Prevent occurence of clock overflow cycle to hang detection
    if (micros() > (_maxUnsignedLong - _maxEchoWaitTime))
        return; // skip detection if value of micros() may overflow

    // Trigger the HC-SR04
    *_pingModeRegister |= _pingPinBitmask;    // Set pin to output (HIGH) in DDR register
    *_pingOutputRegister &= ~_pingPinBitmask; // LOW
    delayMicroseconds(2);
    *_pingOutputRegister |= _pingPinBitmask; // HIGH
    delayMicroseconds(10);
    *_pingOutputRegister &= ~_pingPinBitmask; // LOW

    // Switch port to input mode to detect echo on same pin
    *_pingModeRegister &= ~_pingPinBitmask; // Set pin to input (LOW) in DDR register

    // Stop measuring if another ping is in progress
    if (*_echoInputRegister & _pingPinBitmask)
        return; // pin still high probably other waiting for other echo.

    boolean timeout = false;
    _echoWaitTime = micros() + _maxEchoWaitTime; // max wait timestamp

    // Wait till the ping echo returns or timeout occurs
    while (!(*_echoInputRegister & _pingPinBitmask))
    { // wait till pin changes to HIGH
        if (micros() > _echoWaitTime)
        {
            timeout = true;
            break;
        } // stop waiting if it takes to long based on max ping distance
    }

    // If ping is received, measure pulse duration
    if (!timeout)
    {
        _echoWaitTime = micros() + _maxEchoWaitTime; // max wait timestamp
        _pingStartTime = micros();
        // Wait till ping echo is received
        while (*_echoInputRegister & _pingPinBitmask)
        { // Wait till ping changes to LOW
            if (micros() > _echoWaitTime)
            {
                timeout = true;
                break;
            } // stop waiting if it takes to long based on max ping distance
        }

        if (!timeout)
        {
            _distanceInCM = (micros() - _pingStartTime) / (_speedSoundCmPerUsec * 2);
            _sampleTime = millis();
        }
    }
}

int LandjePingSensor::GetDistanceInCM()
{
    // If recorded last sampled distance is > 2cm
    if (_distanceInCM >= 2)
    {
        // If sample not to old
        if ((_sampleTime - millis()) < _maxSampleLifespan)
            return _distanceInCM; // return distance in cm
        else
            return 0; // return 0 distance
    }
    else
    {
        return 0; // return 0 distamce
    }
}