Robots-For-All (IEEE - HI)

Data Types

There are a number of pre-defined data types available for use, including:

Real Numbers

Real numbers are contained in float and double variables or constants. They are approximations of the desired value based upon what is possible with the limited number of data bits and the base 2 representation. Some numbers and fractions are able to be accurately represented such as (1, 2, 3, 1/2, 1/4, and 1/8). Other numbers are too large, too small or have too many fractional digits. As numbers get large the fractional part of the scientific notation (x.yyy Ezz) starts having too many digits to be properly represented. The same thing happens as numbers get too small.

Irrational numbers such as Pi or e are approximations becase there are too many fractional digits. Some rational numbers such as 1/3 are repeating decimals and others such as 1/10 become repeating decimals when converted to base 2 and as a result are approximations.

Float values use 32-bits to store the value while double values use 64-bits. The approximations get better as the number of bits increase however this comes at a cost of storage space, processing time and error management. The format for these values are defined by IEEE Standard 754 for Binary Floating-Point Arithmetic.

Variables

Variables are user defined named memory locations that contain a value of a specific type. Definitions start with a type followed by the variable name followed by a semicolon. Examples:

bool robotIsRunning;
int32_t xAxisValue;
uint8_t counter;
float lengthInInches;
double latitudeDegrees;

Variable selection: Choose a variable with the range and precision to handle the values of interest. An example: Use an integer to represent money values instead of float or double. Performing math operations on floats and doubles can introduce small errors so that the values won't exactly match an integer value such as zero. This occurs because not all fractional value can be represented exactly with the float or double precision. An example: 1/9 is a decimal point followed by repeating 1. But the data representation has a limited space and truncates when this space is exhausted. Multiplying the variable by 9 generates a decimal point followed by a repeating 9. This value does not equal 1. With money its best to use representation in cents to prevent errors due to precision.

Real Numbers

It is important to understand the difference between compile time optimizations and runtime calculations. Care must be taken when doing runtime math with float and double values to manage the error introduced by the approximated values. An example program is shown below

#include <stdio.h>

void main (int argc, void ** argv)
{
    double da;
    double db;
    double dc;
    float fa;
    float fb;
    float fc;

    float cf = (1. / 3.) * 3.;
    double cd = (1. / 3.) * 3.;

    fa = 1.;
    da = fa;

    fb = 3.;
    db = fb;
    fc = fa / fb;
    dc = da / db;

    printf("fc: %11.9f == 1. ? %s\n", fc, (fc == 1.) ? "true" : "false");
    printf("dc: %17.15f == 1. ? %s\n", dc, (dc == 1.) ? "true" : "false");
    printf("cf: %11.9f == 1. ? %s\n", cf, (cf == 1.) ? "true" : "false");
    printf("cd: %17.15f == 1. ? %s\n", cd, (cd == 1.) ? "true" : "false");
}

Program output:

fc: 0.333333343 == 1. ? false
dc: 0.333333333333333 == 1. ? false
cf: 1.000000000 == 1. ? true
cd: 1.000000000000000 == 1. ? true

Strings

In C and C++ at compile time strings are typically contained within double quotes such as "This is a test." The compiler translates the string of characters into a data area containing the characters followed by a zero (0) termination character. The string variable contains the address of the data area.

An example is shown below:

char * helloWorld = "Hello World!";

                  helloWorld variable                       Data Buffer
        .---------------------------------------.         .-------------.
        | Address of data in read/write section |-------->|Hello World\0|
        '---------------------------------------'         '-------------'

Pointers

Strings are an example of a pointer type variable, note the asterisk (*) in the variable declaration above. Pointers are typically used to manage data structures and arrays but they can also be used for subroutine or function addresses.

Strings are a zero terminated array of characters where the variable is a pointer containing the address of the array. In the example above, the array reference helloWorld[4] fetches the letter 'o' from "Hello".

Constants

Always use constants instead of numbers in your programming! Define the constants at the top of the program or header file. Constants may take two forms:

Constant variables generate a memory referenced at runtime. With appropriate values, defines are integrated into the instructions and may prevent a memory reference. With defines the compiler chooses how to represent the data, whether to place it in one or more instructions or to use a memory reference. When a memory reference is used, the compiler and linker may not be able to reduce all the references to a single value in memory. This latter case is where a constant variable is best.

Data Structures

Data structures are made up of one or more related variables. Below is an example using a global data structure for robotSpeed:

#include <stdint.h>

// Define the data structure
struct robotSpeedStruct
{
    int16_t leftSpeed;
    int16_t rightSpeed;
};

// Global instance of the data structure
struct robotSpeedStruct robotSpeed;

// Subroutine copying data into the data structure
void robotSetSpeed(int16_t leftSpeed, int16_t rightSpeed)
{
    robotSpeed.leftSpeed = leftSpeed;
    robotSpeed.rightSpeed = rightSpeed;
}

New Types

The C and C++ compilers use typedef to define new data types. The new types may duplicate existing types or be data structures, function pointers or classes. Using the new types enables better type checking during compile time. Additionally it is then easy to change a specific type across the entire code base by making a single change to a typedef statement.

The example below defines the new type ROBOT_SPEED as an int16_t, but this could be easily changed to another type such as int8_t if necessary without impacting other variables.

#include <stdint.h>

typedef int16_t ROBOT_SPEED;

// Define the data structure
struct robotSpeedStruct
{
    ROBOT_SPEED leftSpeed;
    ROBOT_SPEED rightSpeed;
};

// Global instance of the data structure
struct robotSpeedStruct robotSpeed;

// Subroutine copying data into the data structure
void robotSetSpeed(ROBOT_SPEED leftSpeed, ROBOT_SPEED rightSpeed)
{
    robotSpeed.leftSpeed = leftSpeed;
    robotSpeed.rightSpeed = rightSpeed;
}

Operators

Operators include arithmetic operators, unary operators, logical operators and conditional operators. Use parenthesis () to specify the order of math operations in a single statement. Operations across multiple statements happen sequentially in the order specified.

Arithmetic Operators

Arithmetic operators include decrement variable by one (--), increment varaiable by one (++), unary plus (+) to return varaiable value and unary minus (-) to return the negative of the variable.

Unary Operators

Uniary operators include add (+), subtract (-), multiply (*), divide (/) and modulo (%).

Bitwise Operators

Bitwise operators include and (&), or (|), xor(^), complement (~) not (!), left shift (<<) and right shift (>>).

Conditional Operators

Conditional operators evaluate one or more expressions until the resulting value is known. Some expressions may be skipped once the value is known. The conditional operators are and (&&) and or (||).

Statements

Statements are single lines followed by a semicolon (;). The statements consist of assignments, control structures or subroutine calls. Multiple statements within braces {} are also considered as a single statement.

Control Structures

Control structures include conditional statements (if/else), switch statements and loops (for, do/while).

    bool done;
    if (a == b)
    {
        mSec = millis();
        done = true;
    }
    else
        done = false;
    bool done;
    switch(a)
    {
    case 0:
        done = false;
        break;

    case 1:
        mSec = millis();
        break;

    case 2:
        mSec = millis();
        done = true;
        break;
    }
    for (index = 0; index < 10; index++)
    {
    }
    while (index < 10)
    {
    }
    do
    {
    } while (index < 10);

Subroutines

Subroutines are a block of code that does not return a value associated with the subroutine name. Subroutines are used by break up the code into smaller chunks to make the code easier to read. Additionally subroutines are used to remove replicated code and replace it with a single copy.

Subroutines may have zero or more input parameters and may return values via pointer parameters.

#include <stdint.h>

int16_t leftMotorSpeed;
int16_t RightMotorSpeed;

// Subroutine copying data into the data structure
void robotSetSpeed(int16_t leftSpeed, int16_t rightSpeed)
{
    leftMotorSpeed = leftSpeed;
    rightMotorSpeed = rightSpeed;
}

Functions

Functions are subroutines that return a value associated with the function name or call. The returned value is specified by the return statement.
#include <stdint.h>

// Function to add two numbers
int16_t addNumbers(int16_t a, int16_t b)
{
    int16_t c;

    c = a + b;
    return c;
}

Function Pointers

Below is an example of defining a function pointer:

#include <stdint.h>
#include <stdio.h>

// Define the function pointer type
typedef void (* R4A_ROBOT_TIME_CALLBACK)(uint32_t deltaMsec);

// Declare the function
void displayTime(uint32_t deltaMsec)
{
    printf("%d (%08x): Milliseconds\r\n", deltaMsec, deltaMsec);
}

// Declare the function pointer variable
R4A_ROBOT_TIME_CALLBACK timeCallback;

// Use the function pointer variable
void main (int argc, void ** argv)
{
    // Initialize the function pointer
    timeCallback = displayTime;

    // Reference the function pointer
    if (timeCallback)
        timeCallback(1000);
}

Below is the program output:

1000 (000003e8): Milliseconds

Classes

Classes are a grouping of related variables, data structures and functions. Classes enable the protection of member variables and functions. The members may be marked as private (accessible only by the class), protected (accessible also by friend classes) or public (accessible by all).

Best Practices

Variables

Define the variable in a single source file. If other source files reference the variable then create an include file that declares the variable as external (extern). Make sure to include this header file in the source file that defines the variable for a compile time type check between the variable and its external declaration. Using the include file in all source files that need the variable prevents multiple external declarations from becoming out of sync and causing issues at runtime that are hard to debug!

Data Structures

Define data structures in an include file. Use this include file in all source files that need the data structure. This prevents data structure definitions in multiple places from becoming out of sync and causing issues at runtime that are hard to debug!

Classes

Always add constructors to initialize all data values! When allocating classes from the heap they are not initialized. As such, bad values in values can cause undesired behavior and bad pointer values can cause CPU crashes (access violations).

Classes that allocate any object from the heap should always use a destructor to free the object.