How is it possible to wait, and check for a button press at the same time? Let's explore one of the solutions to this problem!
Last time we concluded that our traffic light needs a some improvement in order to not annoy drivers. The problem is, that the traffic light turns instantly when somebody presses the button. The solution would be a cooldown for button presses that ensures that the green phase is always long enough. But how do we implement this?
The real problem lays in the fact, that we can't wait using the delay
procedure and detect a button press, at the same time. As the Arduino is a single core processor, it cannot execute two tasks simultaneously. There are still some ways to achieve our goal though, and we will now discover how it can be done.
There are multiple possible solutions. What might struck you is the fact that it is indeed possible to run multiple programs on an old computer which had only one core. This however does not render the fact, that a single core CPU can only execute one task at a time, invalid. This illusion is achieved by switching back and forth between different tasks. If it's done fast enough, the user will not notice it. The question is, can we do this with the Arduino? Well, theoretically yes. The problem is that this would be a very unusual thing to do and it is not supported by the Arduino environment for the Arduino Uno. It would be theoretically possible to implement this, but this is way beyond the scope of this introduction series.
What are the alternatives? We can do two things in parallel, if we let the hardware handle one of them. Classically, one would let the hardware check for the button press. In case of an actual press the execution of other commands is interrupted by the CPU and a special handler function for this button press is called. This technique is called interrupts. Interrupts and the corresponding handler functions are a bit tricky to use, for this reason I have decided to not use them in this introduction series. There is no reason to be sad, at this point, as we have one more possibility left.
We can let the hardware measure the time that has passed since the beginning of the green phase. For this purpose the Arduino Uno has multiple hardware timers. One of these timers runs by default and can be used to get timestamps in milliseconds.
Arduino provides a function called millis
that enables us to obtain such a timestamp.
As always, here is a quick summary of this function:
millis()
How can we use this new function in our code? Well, the returned timestamp does not have an absolute reference point. You can't use it to tell the time, but it allows us to calculate a time difference using a start and an end value. For this we need to store the timestamp at the beginning of the green phase. The time has come to introduce you to variables. A variable allows us to store a value and reference it later under a name that can be defined by us. A C++ variable always has a fixed type and as we have not talked about types already, let's do it first.
There are plenty of types and you can even define them by yourself. These types however are constructed from just a few basic types. Everything on a computer is made up from ones and zeros. For the ease of use and for performance optimizations there are some more high level data types used in practice. There are two sorts of numbers that need to be differentiated: Integers and real numbers. In real numbers there is an infinite amount of numbers in between two numbers like one and two. This makes it very hard to represent this numbers and this is why they are a completely different type of numbers in computer science. In fact, computers can represent them only to a certain degree of accuracy. In C++ there is the standard typefloat
and the more accurate type double
. The more accurate the harder it is to use this types in computations. For all the math nerds among you: floating-point numbers are represented in three parts, the sign, a mantissa and an exponent to the base two. For all of you who don't care, I will outline the consequences quickly: You can express extremely huge numbers as a float, but the accuracy will be pretty low then which means you might get incorrect decimal digits in this range. On the other side you can get a pretty good accuracy and a lot of decimals for small numbers. This is a huge blessing for scientific computations. It however also means, that some numbers can't be represented at all. At least not a hundred percent accurate. Due to this inaccuracy there will always be some small discrepancies when using them in calculations. For this reason it can be dangerous to check to floating-point numbers for equality after doing some calculations with them. They might not match exactly but may only pretty damn close to each other instead, even when mathematically they should be equal. You should also not use them when dealing with money. Use a whole number in cents instead.
Integers are much easier to cope with. What are the base types we have for them? The following table shows an overview:
Type | Range |
---|---|
char (int8_t) | -128 to 128 |
unsigned char (uint8_t) | 0 to 255 |
short/int (int16_t) | -32768 to 32767 |
unsigned short/unsigned int (uint16_t) | 0 to 65535 |
long (int32_t) | -2147483648 to 2147483647 |
unsigned long (uint32_t) | 0 to 4294967295 |
long long (int64_t) | -9223372036854775808 to 9223372036854775807 |
unsigned long long (uint64_t) | 0 to 18446744073709551615 |
Why all these different types? Why not simply use a 64-bit signed integer for everything? The reason is the slow speed in which the microcontroller is able to do computations with these numbers. As the Arduino Uno uses an 8-bit microcontroller and can't cope which such high numbers a lot of work needs to be done in software. Code for doing this will be added by the compiler. This enables us to use these types, anyway. This however, will not only increase the computational overhead, but also requires more storage space in RAM and Flash. On the Arduino we only have a limited amount of them. In consequence, you should only use a big data type if you expect big values. If you only expect positive numbers you can get the maximum out of the data type if you use the unsigned
variant of it.
For these among you who programmed on other platforms before, there is a small trap. On normal computers the type int
refers to a 32-bit integer, on the Arduino it's only a 16-bit integer. Historically the int
type was defined to match the word size of the processor. Nobody knows about this anymore and it is not done consistently in different platforms. For this reason there are alternative names like int32_t
to make the intention of using a 32-bit integer clear. It might be a good style to only use these alternative names, but in practice you will often find the other names too. I will not use these alternative names as their not used in Arduino code found in other places, too.
What happens if a number does not fit into the range of an integer type? In this case we get an integer overflow. This means the number start again from the lowest number of the type. If I increase an unsigned char
variable with a value of 255 by one, I get zero as a result. If I increase it by two, I will get one as a result. For signed data types we will get negative numbers. In fact, the result of our millis
function uses unsigned long
as a data type. The timestamp value will overflow after a little less than 50 days. Luckily we do not deal with this long time ranges in our example, so we can ignore this problem in this tutorial. What would happen is that after 50 days there would be a small period of time when the cooldown period starts again from the beginning,and we need to wait again. It's unlikely that anybody would notice, but in real world applications you should carefully think about whether an integer overflow harms to your project and if you need extra measures to handle this case.
In C++ we also use the type bool
which allows for a value of either true
or false
and is used to store boolean values.
And what about things like texts? Well, each letter is represented as a char
. This gives this data type its name. As text or string as we call it in programming is just a lot of characters after each other. An array of the type char
can be used to represent this. I will cover arrays at a later time, we don't need them here.
How to use a type together with a variable? We can declare a variable for our start timestamp by writing its type and its name.
unsigned long startTime;
We can also assign an initial value. In our case this is the start timestamp. In our case, we obtain this by calling the millis
function. Of course, you can assign a numeric value or another variable to a variable as well, as long as the type allows for it.
unsigned long startTime = millis();
If we don't assign an initial value, the value is undefined. C++ differs from other programming languages at this point. In Java, we would get a value of zero in this case. In C++ you should not rely on an uninitialized variable to have a certain value.
A variable declaration has a certain scope in which it is valid. The variable is declared for the current code block and all sub code blocks. If you declare a variable at top-level outside all functions, it is called a global variable and will be usable in all functions. If you declare a variable in a function or in a loop it will only be usable inside this function or loop. If you declare a variable with the same name in the same scope the compiler will throw an error, complaining about the variable being redeclared. However, if you declare a variable with the same name in a code block below the one where the first declaration happened, the compiler will accept this. If you have a global variable and a local variable with the same name, the local variable will hide the global variable. When using the name it will refer to the local variable. You cannot access the global variable anymore. For this reason, use good names for your variables and don't define them at top-level if you only need them inside a certain function.
Anyway, once we have saved the start timestamp in a variable we can calculate the time difference and use it in a while
loop like this one:
unsigned long startTime = millis();
while(millis()-startTime > 8000) {
}
The loop will be executed for 8000 ms, and we can still work inside it and check for button presses. We need to run the loop as long as either the cooldown has not passed or the button has not been pressed yet. If the button gets pressed, and we still need to continue the loop because the cooldown has not passed, we need to remember later that the button was already pressed. We need to add another variable for it. In this case we can use a variable of the type bool
. We can set it to false
initially and then need to set it to true
inside the loop when the button is pressed. To do so, we need another control structure. For this purpose we can use an if-else
structure.
This structure looks like this:
if(condition) {
statement1;
statement2;
} else {
statement3;
statement4;
}
In case of the condition being fulfilled the first block will be executed, else the second block will be executed. This else
block is optional and can be omitted.
The condition is expressed in the same way as in the while
loop, and we can also write a single statement directly instead of a whole code block as well, if we would want to.
The result is the following piece of code, in with the buttonPressed
variable is already added to the condition of the while
loop:
unsigned long startTime = millis();
bool buttonPressed = false;
while(millis()-startTime < 8000 || !buttonPressed) {
if(digitalRead(2) == LOW) {
buttonPressed = true;
}
}
I added an additional delay of two seconds after our cooldown. This way the traffic light will not instantly become yellow if someone presses the button. In total, we will always get a guaranteed green phase of 10 seconds. At the stage of this additional delay, the button press was already detected and we can use a normal call of the delay
function.
Combined to a full program the new code looks like this:
void setup() {
pinMode(8, OUTPUT); // Green LED
pinMode(9, OUTPUT); // Yellow LED
pinMode(10, OUTPUT); // Red LED
pinMode(2, INPUT_PULLUP); // Button
}
void loop() {
// Green
digitalWrite(8, HIGH);
unsigned long startTime = millis();
bool buttonPressed = false;
while(millis()-startTime < 8000 || !buttonPressed) {
if(digitalRead(2) == LOW) {
buttonPressed = true;
}
}
delay(2000);
digitalWrite(8, LOW);
// Yellow
digitalWrite(9, HIGH);
delay(2000);
digitalWrite(9, LOW);
// Red
digitalWrite(10, HIGH);
delay(10000);
// Red and Yellow
digitalWrite(9, HIGH);
delay(1000);
digitalWrite(9, LOW);
digitalWrite(10, LOW);
}
Let's look at our result. As shown in the video, the traffic light will turn quickly (after 2 s) if no one has pressed it before and also if we wait long enough between two button presses. It is however ensured by the cooldown, that the traffic light is green for at least 10 s. That's great!
I consider the traffic light project to be done at this point. There are however some more things you might want to add. I consider this to be a practice task for you to check if you understood what we learned in all the previous tutorials.
Here are the parts that are still missing: We need an actual pedestrian signal. For this we need to connect two more LEDs to the Arduino. After doing so you need to adjust the code. There are some things to keep in mind at this stage. If the pedestrian signal turns red, there must be enough time for the remaining pedestrians to leave the crossing before the traffic signal turns green. Depending on the country your located in, this might look a bit different. We only have a red light and a green light here, but there are countries like the US which have a flashing 'Don't walk' before the pedestrian signal turns to a steady 'Don't walk'. And even inside the countries themselves there are probably some variations in how the pedestrian signals look like. Just choose the style you like. However, you need to consider that a pedestrian needs to be able to press the button as soon as he is not allowed to cross the street anymore. Only checking for a button press in the green phase is a little too less. In consequence there will be some more places where you need to apply the new scheme of waiting and working you learned in this tutorial.
It's up to you to get this project finished!