Introduction
This article was inspired by the Arduino Challenge here on Codeproject. I read the Ryan's Arduino starter article on binary counter and thought that there were some things which could be improved. I commented on these and Ryan suggested that I could turn this into an article.
So this article presents a binary counter which displays the count using 8 LEDs just as Ryan's counter does but I hope to show some practices which are more suitable for programming microcontroller (MCU) devices such as Arduino. I want to touch on these issues:
- properly connecting LEDs to a microcontroller (MCU) such as Arduino
- using the proper value of the resistor for LED
- using dynamic memory in MCU programs
- using C++ in MCU programs
- considering the code efficiency in MCU programs
Background on LEDs and MCUs
What we need is 8 LEDs connected to the Arduino pins. In Ryan’s article, the LED anodes (positive ends) are connected to Arduino pins and cathodes (negative ends) are connected through a single resistor to GND.
This is not the right way to connect multiple LEDs. There should be a separate resistor for each LED. The problem of using single resistor is that the current flowing through this circuit is always the same, limited by the resistor and this current is divided between all the LEDs which are on at given time. For example, if the resistor is 1000 ohm (1 k), the current will be about 3 mA. If one LED is lit, 3 mA flows through it. If 2 LEDs are lit, 1.5 mA flows through each of them… If all 8 LEDs are lit, about 0.4 mA flows through each of them – which is too little for the LED to be bright.
Why should each LED have a separate resistor?
The current flowing through a LED depends on the voltage. If the voltage is below a certain threshold, the current is almost zero and the LED is off. As the voltage exceeds the threshold, the current quickly rises and we need to limit it using a resistor connected in series with the LED. If there is no resistor, the current will rise too high; the LED will overheat and fail.
The voltage threshold depends on the type of LED, but in general, it is around 2 V. You can (and should) find the real value in the documentation for the LED. You can usually see this right in the description of the LED in the e-shop when you buy it – it is called LED forward voltage. If not, there is usually a link to datasheet in the description. If not, just take your guess based on the color of the LED.
The current which should flow through the LED can also be found in the documentation. It is 20 mA for a “standard” LED and 2 mA for so called low-current or low-power LEDs. The low-power LEDs cost about the same as the standard LEDs and they use 10-times less power, so use them whenever you can.
How to calculate the value of the resistor?
So now we have the two basic values for using a LED – the forward voltage (Vf) and required current (If). We can calculate the value of the resistor (R) to limit the current (If) flowing through the LED:
R = (Vdd – Vf) / If
The Vdd is the voltage on the output pin of the MCU which we use. For Arduino Uno, it is 5 V.
Here is an example. We want to connect a red LED, the Vf is 1.9 V and desired current If is 2 mA (but use Amps in the equation), so If = 0.002 A.
R = (5 – 1.9) / 0.002 = 3.1 / 0.002 = 1550 ohm.
So ideally, we should use a 1550 ohm resistor. But the resistors are manufactured only in certain values, so we will use one which is close and preferably a higher value – higher because then the current will be lower. We could use, for example, 1800 ohm (1k8) or 2200 ohm (2k2). The closest commonly manufactured value of 1500 ohm (1k5) would also be fine.
Now you may sometimes see people connecting LEDs directly to Arduino pins without any resistors - and get away with this. Nothing burns. Well, you should not get used to the idea that if something works, it is right. If you are serious about developing embedded systems which really work, try to understand how every component in your system works and be positively sure that you are using it right. A LED connected to Arduino without the current-limiting resistor does not fail because the MCU is not able to provide enough current to overheat the LED. The pin of the MCU can provide a maximum of about 40 mA in case of Arduino Uno, so the LED will not be too overloaded. But the MCU is not happy to be overloaded like this. You should not rely on the MCU manufacturer protecting the output pins from overload. You can easily burn your Arduino.
Important note about the current from an MCU pin
As usual, you can find the exact numbers in the datasheet but the MCU used in Arduino uno can provide a maximum of 40 mA per pin but not from all the pins at the same time! The total current should not exceed 200 mA per the whole MCU package.
Background on the MCU Software
Writing programs for MCU is still quite a bit different from writing programs for the large and powerful computers. In desktops and laptops, we got used to gigabytes of RAM and gigahertzes of processor (CPU) speed and programming languages like Java or Python which make it easy to write complex programs, but which also use lot of memory and CPU time. This is fine for big computers - the powerful hardware will compensate for inefficient software. In MCUs, things are different. A typical MCU only has megahertz of CPU speed and kilobytes of memory. The efficiency of the program still matters in MCU.
If you ask why, it is because MCUs are used in so many devices which should be cheap, like flashlights, toys, etc. Every cent in price per piece counts if you produce millions of pieces. So the hardware is as cheap as possible and it cannot compensate for inefficient software.
Even if one day the hardware became so cheap that there would be no speed and memory limits, there is still the energy, the power. It's easy to forget it when you write your programs but the program is running on electricity. And the more operations you do in your program, the more energy you use. This is especially important in battery powered devices - which are everywhere around us, take just the remote control for your TV.
So with efficiency in mind, there are two things I didn't like in the Ryan's binary counter - using dynamic memory (new and delete) and converting number to bits by dividing it by two.
Using new and delete
In MCU programming, dynamic memory allocation is not used except for special cases. With so little available RAM you should be aware of how much you really use and for what. There is no virtual and virtually unlimited memory as in big computers. The new operator will not help you get more memory, it will just add overhead for getting and freeing memory which you could allocate statically. Usually, you know how much memory you need. If you need to get some data from the outside world and don't know how big it can be, you still have to design the program so that it works up to some limit and fails gracefully if the data is too big.
Working with bits
In C/C++, it is easy to manipulate and test individual bits. In our binary counter, we have an 8-bit variable which stores the current count and we need to convert this to individual bits to turn on/off the LEDs which show this value. There is no need to use expensive division; the variable is stored in binary in the memory. We just need to see what the value of each bit is. In the code below, I use a loop to convert the number in the counter mCount
to individual bits to be set on output. It is not the most efficient way but hopefully it is readable.
uint8_t mask = 1; uint8_t bitVal;
for(uint8_t i = 0; i < mDigitCount; i++) {
bitVal = (mCount & mask) ? 1 : 0;
digitalWrite(mPins[i], bitVal );
mask = mask << 1; }
Using the Code
Finally, we get to the code. I present three versions of the binary counter. First version is written in good old C language. The other two are in C++.
Why C Language?
Today, C or C++ language is used to program MCUs. There are advantages to using C++, which is object oriented, but there are also disadvantages - the program will probably be a bit larger and less efficient and not all compilers (toolchains) support C++. Even if your compiler supports C++, you cannot expect the same C++ on the big computers. You can expect what someone called “a strange subset of C++”.
So if you ask me, I'd say use C++ only if you really need it – if your program will benefit from this. Here are three reasons for using C++ in MCU programs (but sure, opinions may differ):
- creating a large program which you feel it will be easier to manage in C++
- creating library (nicely wrapped in a C++ class)
- if you need multiple instances of some functionality - as shown in the binary counter in C++ below.
Hardware of the Counter
I used the LEDs connected according to Ryan's article - even though this is not the right way. You can see the right way with separate resistors in this article by Leonid Fofanov.
First version - C language
This is the version I would use for a simple binary counter project without any plans for making this a library or using more instances of the counter.
void CounterIncrement();
void setup()
{
}
void loop()
{
CounterIncrement();
delay(500);
}
void CounterIncrement()
{
static const uint8_t digitCount = 8;
static const uint8_t pins[digitCount] = {12, 11, 10, 9, 8, 7, 6, 5};
static uint8_t count = 0;
static bool firstRun = true;
if ( firstRun ) {
firstRun = false;
for(uint8_t i = 0; i < digitCount; i++) {
pinMode(pins[i], OUTPUT);
}
}
uint8_t mask = 0x80; uint8_t bitVal;
for(uint8_t i = 0; i < digitCount; i++) {
bitVal = (count & mask) ? 1 : 0;
digitalWrite(pins[i], bitVal );
mask = mask >> 1; }
if(count == 255) {
count = 0;
} else {
count++;
}
}
Version 2 - C++
Here is the binary counter written in C++. Besides getting rid of the dynamic memory and slow conversion to binary, I added the option to set the pins which will be used in constructor instead of hardcoding them into the class. So it is easier to use this class with different hardware setup. Also, I added begin
function which is kind of Arduino’s standard function for initialization (I wish they called it init
). It is better to set the pins to output mode in such a function then in the constructor because if we create an object of the class as a global variable, the constructor will be executed sometime early during the program startup and I am not sure if the Arduino pinMode functions which we use are already initialized. So I play it safe and use initialization function which will be called in setup
.
The constructor takes an array of pin numbers as an input parameter together with number of bits used. So it is also possible to have counter with less than 8 bits.
Tip: Try two 4-bit counters instead of one 8-bit counter - see the commented out code for counter2
.
class BinaryCounter {
private:
uint8_t mDigitCount;
uint8_t mPins[8];
uint8_t mCount = 0;
public:
BinaryCounter(uint8_t pins[], uint8_t count);
void begin();
void increment();
};
BinaryCounter::BinaryCounter(uint8_t pins[], uint8_t count)
{
mDigitCount = ( count <= 8 ) ? count : 8;
for(uint8_t i = 0; i < mDigitCount; i++) {
mPins[i] = pins[i];
}
}
void BinaryCounter::begin() {
for(uint8_t i = 0; i < mDigitCount; i++) {
pinMode(mPins[i], OUTPUT);
}
}
void BinaryCounter::increment() {
const uint8_t maxVal = (mDigitCount == 8) ? 255 : (1 << mDigitCount) - 1;
uint8_t mask = 1; uint8_t bitVal;
for(uint8_t i = 0; i < mDigitCount; i++) {
bitVal = (mCount & mask) ? 1 : 0;
digitalWrite(mPins[i], bitVal );
mask = mask << 1; }
if(mCount >= maxVal ) {
mCount = 0;
} else {
mCount++;
}
}
BinaryCounter counter1( (uint8_t[]){5, 6, 7, 8, 9, 10, 11, 12}, 8 );
void setup()
{
counter1.begin();
}
void loop()
{
counter1.increment();
delay(500);
}
Version 3 - C++
One thing I didn't like about the binary counter version 2 above was using the "normal" variable to store the pin numbers. I wanted to use const
variable - but still make it possible to set the pins in constructor. To make this possible, I had to use feature added to C++ only in the C++11 standard to initialize the const
array in constructor. C++11 is supported in Arduino IDE, so no problem here, but I admit it is quite a tough requirement for an MCU program.
So this version uses const
variable for storing the pin numbers which saves 8 bytes of RAM and can lead to a faster, more efficient code because the compiler knows that the pins cannot change during runtime. I also used default parameter values in constructor to make it easier to use the counter with less than 8 pins.
class BinaryCounter {
private:
uint8_t mDigitCount;
const uint8_t mPins[8];
uint8_t mCount = 0;
public:
BinaryCounter::BinaryCounter(uint8_t count, uint8_t p0, uint8_t p1=0, uint8_t p2=0,
uint8_t p3=0, uint8_t p4=0, uint8_t p5=0, uint8_t p6=0,
uint8_t p7=0);
void begin();
void increment();
};
BinaryCounter::BinaryCounter(uint8_t count, uint8_t p0, uint8_t p1, uint8_t p2, uint8_t p3,
uint8_t p4, uint8_t p5, uint8_t p6, uint8_t p7)
: mPins{p0, p1, p2, p3, p4, p5, p6, p7}
{
mDigitCount = ( count <= 8 ) ? count : 8;
}
void BinaryCounter::begin() {
for(uint8_t i = 0; i < mDigitCount; i++) {
pinMode(mPins[i], OUTPUT);
}
}
void BinaryCounter::increment() {
const uint8_t maxVal = (mDigitCount == 8) ? 255 : (1 << mDigitCount) - 1;
uint8_t mask = 1; uint8_t bitVal;
for(uint8_t i = 0; i < mDigitCount; i++) {
bitVal = (mCount & mask) ? 1 : 0;
digitalWrite(mPins[i], bitVal );
mask = mask << 1; }
if(mCount >= maxVal ) {
mCount = 0;
} else {
mCount++;
}
}
BinaryCounter counter1( 8, 5, 6, 7, 8, 9, 10, 11, 12 );
void setup()
{
counter1.begin();
}
void loop()
{
counter1.increment();
delay(500);
}
History
- 2018-06-25: First version