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.
All variables have to have a type declaration. So far, we have only seen
int
data types, some of which we declared asconst
, meaning that the value of the variable cannot change.All functions have to have a type declaration for their return value. We have seen
void
, but we could haveint
or other data types we will introduce here.All commands end in a semicolon.
Every Arduino sketch has a
setup()
and aloop()
function. Thesetup()
function is run upon upload of the compiled sketch and upon reset, and theloop()
function is then run over and over again.The core Arduino libraries have built-in functions and variables. For variables, we have seen
INPUT
,OUTPUT
,HIGH
, andLOW
. We actually also sawLED_BUILTIN
in the setup lesson.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
: Abool
stores a single bit of information. It can take values of 0 or 1. In your sketches, though, use the Boolean constantstrue
andfalse
for the values of abool
.byte
: A byte is an unsigned 8-bit integer. It can take values from 0 to 255; that is 0 to 2⁸ – 1.int
: Anint
in an Arduino sketch is a signed 16-bit integer. That means that the values of anint
range from –32,768 to 32,767; that is –2⁻¹⁵ to 2¹⁵ – 1. Theint
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 asint
s. If you use an external 16-bit ADC, because anint
cannot store 16-bit numbers, you will have to use anunsigned int
.unsigned int
: Anunsigned int
is also a 16-bit integer, but negative numbers are not allowed. Thereforeunsigned int
s 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
: Aword
is equivalent tounsigned int
. Many programmers suggest usingword
instead ofunsigned int
for clarity, but I do not have a strong opinion on this.long
: Along
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 along
, it must be followed by anL
. For example to store the number 3252 in a variable namedshoulderToShoulder
as a long, you would dolong shoulderToShoulder = 3252L
.unsigned long
: Aunsigned long
is an unsigned 32-bit integer. They range from 0 to 4,294,967,295; that is 0 to 2³² – 1. Anunsigned long
must also be followed by anL
when written out as a number.unsigned long
s are very often used when comparing times, since the values returned by themillis()
andmicros()
functions (described below) areunsigned long
s.size_t
: This is a special data type used to represent the size of a variable (or any object, really) in bytes.float
: Afloat
in an Arduino sketch is a 32-bit floating point number. Afloat
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 int
s, 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 achar
. When defining achar
, it is in single quotes, e.g.,char myGradeInBE189 = 'A';
.string
: Astring
is an array ofchar
s. They are constructed using double quotes.char myString[] = "Hello, world."
string
s 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 forString
objects, doing concatenation instead.-
: Subtraction.*
: Multiplication./
: Division%
: Mod.6 % 3
gives0
.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 |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 byte
s instead of int
s. 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., bothint
s, bothbyte
s, bothlong int
s, etc.) are compared. The result has a1
in a given bit position if both numbers have a1
in that position. So,x & y
gives12
, which has a binary representation of00001100
.|
: Bitwise OR. Two numbers with the same number of bits are compared. The result has a1
in a given bit position if either of the numbers has a1
in that position. So,x | y
gives207
, which has a binary representation of11001111
.^
: Bitwise XOR. Two numbers with the same number of bits are compared. The result has a1
in a given bit position if the two numbers have different numbers in that position. So,x ^ y
gives195
, which has a binary representation of11000011
.
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 onbyte
s, but onint
s. The compiler will case thebyte
variable as anint
in order to perform the~
operation. Remember that anint
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 variablex
as anint
has representation0000000001001101
. 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 of1111111110110010
. For anyint
x
,~x
is equivalent go-x - 1
.<<
: Left bitshift. This operation slides the1
s and0
s 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 gives616
. Wait….. WHAT AGAIN!? The result of a bitshift operation is anint
. So, this bitshift results in0000001001101000
, which has a decimal value of 616. Conversely,x << 13
gives-24576
, which has a binary representation of1010000000000000
. Note that the second operand (in our case, we used3
and13
) must be less than or equal to 32.>>
: Right bitshift. This operations slides1
s and0
s 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 anint
. That means that negative integers are padded with1
s and positive integers are padded with0
s. This method of padding is called sign extension. So,x >> 3
gives 9, which has a binary representation of0000000001001
. Conversely, consider a right bitshift on-77
, which has a binary representation of1111111110110011
.-77 >> 3
gives-10
, which has a binary representation of1111111111110110
.
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.
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.The cases can be either
char
s orint
s.You can omit the
default
case if you like.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.
The definition of the function must start with the data type of what the function returns. If it returns nothing, use
void
.Each argument of the function needs to have its type declared in the function declaration.
The body of the function follows the definition statement and is contained in braces.
It is good practice to start your function body with comments saying what it does.
If a function returns something, this is accomplished with a
return
statement.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.