Robots-For-All (IEEE - HI)
Data Types
There are a number of
pre-defined data types
available for use, including:
-
Integers (intX_t, where X = 8, 16, 32, 64 defining the number of bits):
- 8 bit: -128 to 127
- 16 bit: -32768 to 32767
- 32 bit: -2147483648 to 2147483647
- 64 bit: -9223372036854775808 to 9223372036854775807
- Unsigned integers (uintX_t, where X = 8, 16, 32, 64 defining the number of bits)
- 8 bit: 0 to 255
- 16 bit: 0 to 65535
- 32 bit: 0 to 4294967295
- 64 bit: 0 to 18446744073709551615
- Real numbers
- float - About 7 digits of precision, Range: 1.4 E-45 to 3.4 E38
- double - About 15 digits of precision, Range: 4.9 E-324 to 1.8 E308
- bool - Logical true / false values
- char - Represent character data such as 'a'
- string - A string of characters such as "This is a test."
- pointer - Address of a variable or routine.
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:
- Variable:
const uint32_t quarter = 25; |
- Define:
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.