5. Programming Arduino


In this lesson, we will take a whirlwind tour through the syntax you need as you write sketches. This lesson is long, and I thought about breaking it up, but I think it will be helpful to have a single quick reference to go back to, in addition to the exceedingly useful Arduino language reference.

When you finish this lesson, it is important to proceed without much delay to the next lesson, where you will put some of the aspects of Arduino programming learned here to use.

Review of last lessons

You will remember in the previous lessons that we have already established a few key syntactical rules.

  1. All variables have to have a type declaration. So far, we have only seen int data types, some of which we declared as const, meaning that the value of the variable cannot change.

  2. All functions have to have a type declaration for their return value. We have seen void, but we could have int or other data types we will introduce here.

  3. All commands end in a semicolon.

  4. Every Arduino sketch has a setup() and a loop() function. The setup() function is run upon upload of the compiled sketch and upon reset, and the loop() function is then run over and over again.

  5. The core Arduino libraries have built-in functions and variables. For variables, we have seen INPUT, OUTPUT, HIGH, and LOW. We actually also saw LED_BUILTIN in the setup lesson.

  6. Comments start with // or appear between /* and */.

We will now proceed to learn about programming the Arduino microcontroller.

A general note about programming in Arduino: keep it simple

You may have experience coding in C++ and have used all of its rich object orientation in your code. You may have constructed complicated, but powerful data types. You can do a lot of that with Arduino, but I have found that it is easier not to. The main reason is that you have only 2 kB of RAM to work with. This means that if you are doing to do something like dynamic memory allocation, which you would do in almost any C++ program, you should try to accomplish the same task in ARduino with static arrays. Unforeseen problems, such as memory fragmentation, can rear their ugly heads.

It is safe to use pointers and work with them, e.g., to make two-dimensional arrays, but we will not discuss them here. Nonetheless, be careful when doing so.

Since we ultimately will be using the Arduino Uno to run and instrument and collect and send data, we will be connecting it to your computer. You computer is much more powerful that the Arduino Uno (that’s why it probably costs two orders of magnitude more). Furthermore, you can program whatever manipulations you need to do to the data in Python, which, being a high-level interpreted language, is much easier to quickly write functional code with than C++.

With that in mind, let us proceed to learn about programming Arduino.

Data types

As I have mentioned, every variable in an Arduino sketch has a type, and that type must be declared when the variable first appears. It is ok to declare it before you use it. That is, this is ok:

int j;

j = 0;

as is this:

int j = 0;

They give the same result.

Scalar data types

  • bool: A bool stores a single bit of information. It can take values of 0 or 1. In your sketches, though, use the Boolean constants true and false for the values of a bool.

  • byte: A byte is an unsigned 8-bit integer. It can take values from 0 to 255; that is 0 to 2⁸ – 1.

  • int: An int in an Arduino sketch is a signed 16-bit integer. That means that the values of an int range from –32,768 to 32,767; that is –2⁻¹⁵ to 2¹⁵ – 1. The int is by far the most-used variable type. Note that the 10-bit ADC converts voltages to 10-bit integers that range from 0 to 1023. You can safely store these as ints. If you use an external 16-bit ADC, because an int cannot store 16-bit numbers, you will have to use an unsigned int.

  • unsigned int: An unsigned int is also a 16-bit integer, but negative numbers are not allowed. Therefore unsigned ints can range from zero to 65,535; that is 0 to 2¹⁶ – 1, which means you an store a 16-bit integer with it.

  • word: A word is equivalent to unsigned int. Many programmers suggest using word instead of unsigned int for clarity, but I do not have a strong opinion on this.

  • long: A long is a signed 32-bit integer. They range from –2,147,483,648 to 2,147,483,647; that is –2⁻³¹ to 2³¹ – 1. To denote an integer as a long, it must be followed by an L. For example to store the number 3252 in a variable named shoulderToShoulder as a long, you would do long shoulderToShoulder = 3252L.

  • unsigned long: A unsigned long is an unsigned 32-bit integer. They range from 0 to 4,294,967,295; that is 0 to 2³² – 1. An unsigned long must also be followed by an L when written out as a number. unsigned longs are very often used when comparing times, since the values returned by the millis() and micros() functions (described below) are unsigned longs.

  • size_t: This is a special data type used to represent the size of a variable (or any object, really) in bytes.

  • float: A float in an Arduino sketch is a 32-bit floating point number. A float can take values between –3.4028235×10³⁸ and 3.4028235×10³⁸. It provides six or seven digits of precision.

Note that on an Ardunio Uno, there is no double data type. Note also that the data type short is equivalent to int.

Arrays

As I mentioned above, we will not discuss complicated array structures. For this class, simple static one-dimensional arrays will suffice.

Declaring arrays

There is no array data type. Rather, an array is an ordered collection of elements of a single data type. An array is declared with the data type of its elements. To declare an array of 5 ints, the syntax is

int myArray[5];

Indexing an array

To access the elements of an array, I use indexing with brackets. Indexing in C++ is 0-based.

int myArray[4];

myArray[0] = 3;
myArray[1] = 2;
myArray[2] = 5;
myArray[3] = 2;

If I asked for myArray[4], which does not exist, the compiler will not give an error, and I’ll get weird results when I run the code. It is therefore very important that you are careful not to overrun an array.

Directly creating an array

Alternatively, I can create an array using braces.

int myArray[4] = {3, 2, 5, 2};

When creating an array in this case, I do not need to specify how many entries; the compiler will infer it.

int myArray[] = {3, 2, 5, 2};

Determining the number of elements in an array

Later on in a program, I might like to know how many entries are in an array. There is no built-in function to do that. But you can use the sizeof() function, which returns the number of bytes that an object occupies in memory. Since every entry in an array is of the same type, you can use the sizeof() function to get the number of entries by dividing the size of the array by the size of an entry.

int lenArray = sizeof(myArray) / sizeof(myArray[0]);

Mutating arrays

Because I do not have const in front of the declaration, I can change the values of entries in the array.

int myArray[4] = {3, 2, 5, 2};

myArray[3] = 3;

// myArray is now {3, 2, 5, 3}

Using entries of an array in a calculation

I can also access entries of an array with indexing and use their values.

int myArray[4] = {3, 2, 5, 2};

int mySum = myArray[0] + myArray[1] + myArray[2] + myArray[3];

// mySum stores the value 12.

I can also pluck values out of an array and store them.

Character and string data types

  • char: A single character, like 'A' is stored as a char. When defining a char, it is in single quotes, e.g., char myGradeInBE189 = 'A';.

  • string: A string is an array of chars. They are constructed using double quotes. char myString[] = "Hello, world." strings are null-terminated, which means that the last element of the array of characters is a special character called a null character. This character is \0. It is important because it lets functions that work with strings know where the string ends.

The String class

While I gave a warning about using C++’s object orientation, the core libraries have some handy classes that you can make use of. The String class is particularly useful.

To construct a String instance, you use String() as a function. You have to make sure to declare your variable to be of a String type.

String myString = String("Hello, world.");

The variable myString now has many methods to use. Here are a few:

String myString = String("Hello, world.");

// Gives 13
int lenString = myString.length();

// Converts to upper case in place.
myString.toUpperCase();

// Gives 5
int commaIndex = myString.indexOf(',');

You can also use operators with the string.

String myString = String("Hello, world.");

// You can use + to concatenate with instantiating
String greeting = String("Hello, world." + " Pleased to meet you.");

// Or after instantiation
String greeting = String(myString + " Pleased to meet you.");

You can also convert a number to a string, and a string to a number.

// Convert an integer
String intAsString = String(3252);

// And bring it back to an integer
int favoriteInt = intAsString.toInt();

// If you convert a float, the second argument is how many decimal places to keep.
// This gives "3.14".
String floatAsString = String(3.14159, 2);

// You can convert back to a float. This will give 3.14.
float pi = floatAsString.toFloat();

Unless you are RAM constrained, you should just use String objects when handling strings. They have a bigger footprint in RAM, but the benefits are substantial.

Type conversion

You can change types of a variable using casting. To cast, place the data type you want to convert to in parentheses before the variable you are converting. Here are a few examples.

int myInt = 176;
float myFloat = 2.718;

// Convert an int to a byte. Be sure it's ≤ 255
byte intAsByte = (byte) myInt;

// Convert an int to a float
float intAsFloat = (float) myInt;

// Convert a float to an int (gives 2; chops off decimal)
int floatAsInt = (int) myFloat;

Operators

Now that we have a handle on data types, we can look into operators.

Arithmetic operators

We have already seen the assignment operator, =, and it works as you might intuit. Other arithmetic operators are:

  • +: Addition. It is overloaded for String objects, doing concatenation instead.

  • -: Subtraction.

  • *: Multiplication.

  • /: Division

  • %: Mod. 6 % 3 gives 0. 6 % 4 gives 2.

Note that there is no floor division. This can be accomplished with casting, e.g., (int) 6 / 4 will give 1.

There is also no raise-to-power operator. This is achieved using the pow() function.

Comparison operators

These are the same as in Python and give a bool, either true or false, as a result.

  • ==: Equal to.

  • !=: Not equal to.

  • <: Less than.

  • >: Greater than.

  • <=: Less than or equal to.

  • >=: Greater than or equal to.

Boolean operators

The three Boolean operators are:

  • &&: AND

  • ||: OR

  • !: NOT

Here are some examples.

// Gives true
4 < 6 && 7 < 93

// Gives true
4 < 6 || 7 < 93

// Gives false
4 < 6 && 7 > 93

// Gives true
4 < 6 || 7 > 93

// Gives false
!(4 < 6 || 7 > 93)

Compound operators

Compound operators are used to change the values of variables in place. The operators are best explained by considering equivalent expressions.

compound operator expression

equivalent expression

x += 1

x = x + 1

x++

x = x + 1

x -= 1

x = x - 1

x--

x = x - 1

x *= 2

x = x * 2

x /= 2

x = x / 2

x %= 2

x = x % 2

Bitwise operators

Bitwise operations work on numbers at the level of their binary representations. While I have not used these operators very often in my various hacking pursuits, they do come up fairly often in programming devices, especially in the context of communications where data are sent and received as bits and bytes.

I think the operators are best explored by example. To keep the binary representations short, we will use bytes instead of ints. Say we have a byte variable 01001101. This corresponds to the decimal number 77. The number 10001110 is 142. Let us now consider some bitwise operators on these two numbers to see how they work. It helps to look at these two binary numbers on top of each other to make easy comparisons

x = 77:  01001101
y = 142: 10001110

In what follows, assume that we have already executed the code

byte x = 77;
byte y = 142;
  • &: Bitwise AND. Two numbers with the same number of bits (e.g., both ints, both bytes, both long ints, etc.) are compared. The result has a 1 in a given bit position if both numbers have a 1 in that position. So, x & y gives 12, which has a binary representation of 00001100.

  • |: Bitwise OR. Two numbers with the same number of bits are compared. The result has a 1 in a given bit position if either of the numbers has a 1 in that position. So, x | y gives 207, which has a binary representation of 11001111.

  • ^: Bitwise XOR. Two numbers with the same number of bits are compared. The result has a 1 in a given bit position if the two numbers have different numbers in that position. So, x ^ y gives 195, which has a binary representation of 11000011.

We will not consider operations that work on the binary representation of a single number.

  • ~: Bitwise NOT. I call this operator a bit-flipper, since it flips the bits of a number, changing a 1 to a 0 and a 0 to a 1. ~x gives -78. Wait…. WHAT!? It turns out that the ~ operator does not work on bytes, but on ints. The compiler will case the byte variable as an int in order to perform the ~ operation. Remember that an int is a signed integer, meaning that one of the bits is used to indicate the sign of the number. By convention, that is the first bit, the so-called highest bit. The variable x as an int has representation 0000000001001101. The highest bit is the sign, which is zero in this case, indicating a positive number. Applying bitwise NOT gives -78, which has a binary representation of 1111111110110010. For any int x, ~x is equivalent go -x - 1.

  • <<: Left bitshift. This operation slides the 1s and 0s leftward a set number of steps, padding the right bits with zeros and discarding bits that get shifted off to the left. x << 3 will shift the bits three places to the left and gives 616. Wait….. WHAT AGAIN!? The result of a bitshift operation is an int. So, this bitshift results in 0000001001101000, which has a decimal value of 616. Conversely, x << 13 gives -24576, which has a binary representation of 1010000000000000. Note that the second operand (in our case, we used 3 and 13) must be less than or equal to 32.

  • >>: Right bitshift. This operations slides 1s and 0s rightward a set number of steps, discarding bits that are shifted off to the right. Be careful, the bits that are padded to the left are whatever the highest bit is if you are using an int. That means that negative integers are padded with 1s and positive integers are padded with 0s. This method of padding is called sign extension. So, x >> 3 gives 9, which has a binary representation of 0000000001001. Conversely, consider a right bitshift on -77, which has a binary representation of 1111111110110011. -77 >> 3 gives -10, which has a binary representation of 1111111111110110.

Conditionals

We have now seen the various data types and operations we can do with them. Let us now turn our attention to control flow, starting with conditionals.

if statements

An if statement has the following syntax.

if (expression_that_evaluates_to_a_bool) {
  // Expressions that are evaluated in the case of true
}
else {
  // Expressions that are evaluated in the case of false
}

The else clause may be omitted. You may also chain if statements together.

if (expression_that_evaluates_to_a_bool) {
  // Expressions that are evaluated in the case of true
}
else if (another_expression_that_evaluates_to_a_bool) {
  // Expressions that are evaluated in the case of true for another expression
}
else {
  // Expressions that are evaluated in the case of falses for all if's
}

You can have as many else if blocks as you like.

switch cases

We can also use switch-cases as conditionals. Let’s say we have a char variable called var that can take on 26 values, 'A', 'B', 'C', etc. We want to do specific tasks when the value is 'A', 'B', or 'C', and a different task if it is any other letter. A switch-case accomplishes this.

switch(var) {
  case 'A':
    // Do what I want for 'A'
    break;
  case 'B':
    // Do what I want for 'B'
    break;
  case 'C':
    // Do what I want for 'B'
    break;
  default:
    // Whatever I want to do for the others
    break;
}

A few notes.

  1. Note the keyword break. It is necessary in the switch-case and it stops evaluation of the switch case. break is also used to break out of loops, which we will soon see.

  2. The cases can be either chars or ints.

  3. You can omit the default case if you like.

  4. The above will give the same result as:

if (var == 'A') {
  // Do what I want for 'A'
}
else if (var == 'B') {
  // Do what I want for 'B'
}
else if  (var == 'C') {
  // Do what I want for 'C'
}
else {
  // Whatever I want to do for the others
}

Switch-cases are typically preferred when evaluating fixed data values, whereas if statements are preferred when evaluating expressions that give a Boolean.

Iteration

There are three structures for iteration, a for loop, a while loop, and a do-while loop.

for loops

A for loop is used to do a task over and over again a set number of times. A for loop looks like this:

for (initialization; condition_to_keep_going; increment) {
  // Stuff you want to do over and over
}

The initialization happens when the loop is first entered. Usually, it initializes a counter that you will increment in a for loop. The condition is tested at the beginning of each iteration through the loop. If the expression for the condition must give true or false. If it gives false, iteration is terminated. Finally, the increment statement is run after each cycle of the loop.

For example, to make an LED flash exactly ten times, you might do this.

for (int i = 0; i < 10; i++) {
  digitalWrite(ledPin, HIGH);
  delay(250);
  digitalWrite(ledPin, LOW);
  delay(250);
}

Note that if the body of the loop is a single line, it can come after the for statement and end with a semicolon. For example, if we wanted to do something silly, like a series of delays, we could do:

for (int i = 0; i < 10; i++) delay(250);

while loops

A while loop is used when you want to do a task over and over again until a condition is met. Its syntax is:

while (condition_to_keep_going) {
  // Do want you want to do
}

At the start of each iteration, the condition is checked. If it evaluates false, iteration is terminated. Inside the block of the while loop, it is important that the condition changes, otherwise the while loop will never stop. The ten-flashes of an LED written using a while loop looks like this.

int i = 0;

while (i < 10) {
  digitalWrite(ledPin, HIGH);
  delay(250);
  digitalWrite(ledPin, LOW);
  delay(250);
  i++;
}

Similar to for loops, one-liner bodies can come directly after the while statement, with the braces being dispensable.

do-while loops

A do-while loop is just like a while loop, except the condition is evaluated at the end of the loop. This means that the body of the loop is evaluated at least once, on the first iteration. The syntax is:

do {
  // Do want you want to do
} while (condition_to_keep_going);

Here is the flash-the-LED-ten-times code as a do-while loop.

int i = 0;

do {
  digitalWrite(ledPin, HIGH);
  delay(250);
  digitalWrite(ledPin, LOW);
  delay(250);
  i++;
} while (i < 10);

Breaking and continuing

In any of the three types of loops, the break keyword will stop iteration and break out of the loop, heading to the next part of the program. If the continue keyword is encountered within the body of a loop, the rest of the body is ignored and then iteration continues.

Functions

As with design of devices, it is useful to practice modular design in the design of software. If you have a task that will be done more than once in a sketch, or that you might use in other sketches, you should write it as a function.

It is easiest to see how functions are defined by looking at one. Here is a function to flash an LED n times.

void flashLED(int ledPin, int n) {
  /*
   * Flash an LED controlled via `ledPin` `n` times.
   */

  for (int i = 0; i < 10; i++) {
    digitalWrite(ledPin, HIGH);
    delay(250);
    digitalWrite(ledPin, LOW);
    delay(250);
  }

}

And here is another function that give the sum or an array of integers.

int intSum(int ar[], int lenArray) {
  /*
   * Compute the sum of an array of integers. Doesn't check
   * if the sum overruns the maximum value of an int.
   */
  int sum = 0;

  for (int i = 0; i < lenArray; i++) {
    sum += ar[i];
  }

  return sum;
}

Referring to the above two functions, here are some syntactical rules for functions.

  1. The definition of the function must start with the data type of what the function returns. If it returns nothing, use void.

  2. Each argument of the function needs to have its type declared in the function declaration.

  3. The body of the function follows the definition statement and is contained in braces.

  4. It is good practice to start your function body with comments saying what it does.

  5. If a function returns something, this is accomplished with a return statement.

  6. Variables used within functions need to be declared with type declarations. By default, the scope of these variables is limited to the function in which they are declared.

Onwards!

There are many many more features to C++, and therefore many more ways you can program Arduino. However, the basics here are sufficient to build quite powerful programs and rapidly increase the speed of your prototypes.

As always, just reading this will not make these ideas stick. You need to practice. I advise you to proceed directly to the next lesson.