TVZ Mechatronics Team


Zagreb University of Applied Sciences, Professional Study in Mechatronics

Writing your own mbed library

Creating a HC-SR04 class

Although there are already a few classes available, begin to build your own class for ultrasonic distance measurement. This is a suitable way of learning to develop your own classes and libraries.

First take a look at the revision 0 of the program above. The main.cpp file contains the following parts, which combined together perform the distance measurement:

  • 2 microcontroller pins (p5 and p7),
  • 3 main objects (echo, trigger and timer),
  • 2 functions (startTimer() and stopTimer()),
  • configuration of the rising and falling edges for the echo object at the beggining of the main.cpp file,
  • part of the code for starting the measurement and calculation of the distance.

Now we are trying to integrate these observations into the class. The class contains its declaration and implementation parts. We will first write the declaration part of the class.

Declaration part of the class

First we declare a class named HC-SR04. In the public part of the class declaration we declare a constructor. From the above observations we know that we will instatiate our HC-SR04 objects with 2 pins, so our constructor needs to receive two PinName values, e.g. echoPin and triggerPin. Document the meaning of these two values by using the @param markup.

Next, we integrate our 3 objects into the private part of the class declaration, to protect them from changing outside the class in an unwanted way. Note that you can not initialize the objects or variables at the declaration part of the class, i.e. you cannot assign a pin to the echo and trigger objects at this point. We will do that later in the constructor implementation.

Next, we add our 2 functions to the class declaration. We must decide if the functions will be public or private. In this case, the users of our class don't need to use these functions explicitly, so they can be declared as private. When coppying the functions headers, also copy the initial comments. They will later serve for the purpose of class documentation.

Next, the configuration of the rising and falling edges for the echo object, as well as all other configurations, will be implemented later in the constructor init() function. So for now, just add the declaration of init() function in the private part of the class declaration.

Finally, add the declarations of functions for starting the measurement and calculating and returning the distance. Figure out which one should be private and which one should be public. Add the distance variable in the private part of the class declaration.

The declaration of the class should now look something like this:

class HCSR04 {
    
    public:
    
    /** Receives two PinName variables.
     * @param echoPin mbed pin to which the echo signal is connected to
     * @param triggerPin mbed pin to which the trigger signal is connected to
     */
    HCSR04(PinName echoPin, PinName triggerPin);
    
    /** Calculates the distance in cm, with the calculation time of 25 ms.
     * @returns distance of the measuring object in cm.
     */
    float getDistance_cm();
    
    private:
    
    InterruptIn echo;       // echo pin
    DigitalOut trigger;     // trigger pin
    Timer timer;            // echo pulsewidth measurement
    float distance;         // store the distance in cm
    
    /** Start the timer. */
    void startTimer();
    
    /** Stop the timer. */
    void stopTimer();
    
    /** Initialization. */
    void init();
    
    /** Start the measurement. */
    void startMeasurement();
};

We can now move to the class implementation.

Implementation part of the class

We are implementing the constructor first. As we mentioned in the declaration part of the class, all initializiations must be performed in the constructor. The pin assigment is mandatory here and it is carried out by using the : operator after the constructor header (line 1 from the code below). All other initializations are carried out in the init() function, which is called immediately in line 2 of the code:

HCSR04::HCSR04(PinName echoPin, PinName triggerPin) : echo(echoPin), trigger(triggerPin) {
    init();
}

Why is it a good idea to call another function, like init(), instead of writing the initialization code directly in the constructor? We can later decide to have two or more constructors with some other options. If we would write the initialization code directly in the constructor, then we would have to copy-paste it in every constructor, which is not very practical thing to do. Now let's see the implementation of the init() function:

void HCSR04::init() {
    /** configure the rising edge to start the timer */
    echo.rise(this, &HCSR04::startTimer);
    
    /** configure the falling edge to stop the timer */
    echo.fall(this, &HCSR04::stopTimer);
    
    distance = -1; // initial distance
}

We now have a this pointer with configuration of the rising and falling edges on the echo pin. The reason for this is that the class member functions are stored in a separate place in memory than objects. When an object calls the function, it must pass its own address to that function, so that the function knows on which object it will perform its tasks. The address of the object is stored in the this pointer.

Another thing to notice is that the distance variable is initialized to an unrealistic value of -1. This value serves the programmer to recognize that measurements have not yet been made or that another event has occured. It is a good programming practice to initialize every variable in the constructor.

The remaining implementations are fairly easy to transfer from the main.cpp file of the revision 0 of the program. The revision 1 of the program contains the class declaration and implementation, as well as the main() function with instantiated object and measurement example.

Separating into files

The next logical step is separating the class declaration, implementation and main() function into files. Create two new files in your program folder. Name them HCSR04.h (for declaration part of the class) and HCSR04.cpp (implementation part). Then create a new library named HCSR04 and move the created two files in the library. Then cut and paste the appropriate code from the main.cpp in the library files and try to compile the program. Obviously, we need to add some information to the compiler, so it knows where to look for our code.

Let's first look at our current main.cpp file.

main.cpp

/** Revision 2: Separating the class declaration, implementation and main() function into files.  */

#include "mbed.h"

Serial pc(USBTX, USBRX);    // communication with terminal

int main() {
    HCSR04 sensor(p5, p7);     // instantiate the sensor object
    while(1) {
         /** Print the result in cm to the terminal with 1 decimal place
         *  (number 5 after % means that total of 5 digits will be reserved
         *  for printing the number, including the dot and one decimal place). */
        pc.printf("Distance: %5.1f cm\r", sensor.getDistance_cm());
        wait_ms(1000-25);
    }
}

The compile time error is caused in the line 8, because the comiler doesn't know about the existence of the class HCSR04. We explicitly need to tell the compiler to include it. So, on the line 4 simply add:

Add to line 4 of main.cpp

#include "HCSR04.h"

The HCSR04.h is ok as it is, but at this point we want to add some description of the class for auto-generating documentation. We usually add some sample code with basic usage of the class (like our main.cpp file), because other users will probably import the library into their own programs. The HCSR04.h file should look something like this:

HCSR04.h

/** A distance measurement class using ultrasonic sensor HC-SR04.
 *  
 * Example of use:
 * @code
 * #include "mbed.h"
 * #include "HCSR04.h"
 *
 * Serial pc(USBTX, USBRX);    // communication with terminal
 * 
 * int main() {
 *     HCSR04 sensor(p5, p7);     // instantiate the sensor object
 *     while(1) {
 *         pc.printf("Distance: %5.1f cm\r", sensor.getDistance_cm());
 *         wait_ms(975); // print the result every 1 second
 *     }
 * }
 * @endcode
 */
class HCSR04 {
    
    public:
    
    /** Receives two PinName variables.
     * @param echoPin mbed pin to which the echo signal is connected to
     * @param triggerPin mbed pin to which the trigger signal is connected to
     */
    HCSR04(PinName echoPin, PinName triggerPin);
    
    /** Calculates the distance in cm, with the calculation time of 25 ms.
     * @returns distance of the measuring object in cm.
     */
    float getDistance_cm();
    
    private:
    
    InterruptIn echo;       // echo pin
    DigitalOut trigger;     // trigger pin
    Timer timer;            // echo pulsewidth measurement
    float distance;         // store the distance in cm
    
    /** Start the timer. */
    void startTimer();
    
    /** Stop the timer. */
    void stopTimer();
    
    /** Initialization. */
    void init();
    
    /** Start the measurement. */
    void startMeasurement();
};

We still cannot compile our program, because implementation file of the class does not contain the member function prototypes or simply class declaration, as well as the prototypes of functions from mbed.h library. This is also easily solved by adding the #include statements at the beggining of the HCSR04.cpp file:

HCSR04.cpp

#include "mbed.h"
#include "HCSR04.h"

HCSR04::HCSR04(PinName echoPin, PinName triggerPin) : echo(echoPin), trigger(triggerPin) {
    init();
}

void HCSR04::init() {
    /** configure the rising edge to start the timer */
    echo.rise(this, &HCSR04::startTimer);
    
    /** configure the falling edge to stop the timer */
    echo.fall(this, &HCSR04::stopTimer);
    
    distance = -1; // initial distance
}

void HCSR04::startTimer() {
    timer.start(); // start the timer
}

void HCSR04::stopTimer() {
    timer.stop(); // stop the timer
}

void HCSR04::startMeasurement() {
    /** Start the measurement by sending the 10us trigger pulse. */
    trigger = 1;
    wait_us(10);
    trigger = 0;
    
    /** Wait for the sensor to finish measurement (generate rise and fall interrupts).
     *  Minimum wait time is determined by maximum measurement distance of 400 cm.
     *  t_min = 400 * 58 = 23200 us = 23.2 ms */
    wait_ms(25); 
    
    /** calculate the distance in cm */
    distance = timer.read() * 1e6 / 58;
    timer.reset(); // reset the timer to 0 after storing the distance
}

float HCSR04::getDistance_cm() {
    startMeasurement();
    return distance;
}

Our code can now be compiled without errors or warnings. One last thing to do is to add so called define guards in the declaration part of the class, to prevent multiple inclusion of our library in some other program. The complete program and developed library is published as revision 2 of our program. Note that a library is now separated from our initial program and can be modified without changing our main program. Or we can start a new program and import this library.

Import libraryHCSR04

A distance measurement class using ultrasonic sensor HC-SR04.

At this point of library development, we have the initial revision of the library and 2 small revisions related to the documentation. So switch to revision 2 of the library and have a look at the auto-generated documentation. Everything should be clear enough. If we are satisfied with the current state of the library and its documentation, we can begin with testing the sensor and use it in some real application.

Improving the library

While testing the library with the HC-SR04 ultrasonic sensor, we notice some strange readings when the measurement range is below 4 cm and above 400 cm. These values should be set as sensor limit values, and should be interpreted as <4 cm and >400 cm. Try to implement these limitations into our class. The possible solution is given in the revision 3 of the library.

Next, add a member function which enables the user to set these limits within the manufacturer declared range of 2 cm and 400 cm. Adjust the default settings of the class to be in this manufacturer declared range. In addittion, enable the user to get the distance in the units of milimeters, and to get the minimum and maximum distance set by the user. The possible solution is given in the revision 5 of the library, and the example of usage is shown in the revision 3 of the program.

Try to notice the classic example of a bug in the member function setRanges() and implement the function correctly.

Usage of the library with filtering

Revision 5 of the program shows the usage of the library with basic filtering using PT1 filter with a time constant of 2 seconds. The measuring results of longer distances (>100 cm) are now more stable, but when the distance suddenly changes, filtered results need about 10 seconds to stabilize the measurement (approximately 5 time constants). Such delay must be taken into account when designing a control system with this sensor and filter.

Congratulations!

You have completed all the exercises in the An example of writing your own mbed library topic.

Return to TVZ Mechatronics Team Homepage.


All wikipages