Minimal Code Arduino Traffic Light

by Gilissen Erno in Circuits > Arduino

387 Views, 0 Favorites, 0 Comments

Minimal Code Arduino Traffic Light

mega.jpg

Arduino is a nice way to start with the concept of micro controllers, but the decoupling of the hardware does not always make code more readable or less complex than it should/could be. As an example: there are many traffic lights implementations on Arduino. All code is immense complex to read simply because users are no longer aware of the micro controller behavior. This is an example it really can pay off to dig a bit deeper and learn about the hardware to avoid software is way more complex than it should / has to be.

Preface

The targeted Arduino's are Uno & Mega, but basically any micro controllers are similar: what is called digital or analog I/O are typically called Port pins because they connect the core (the unit that executes software instructions) with the outside world. Since these Arduino's are build around the 8-bit AVR micro controller, one should download the respective datasheet from Microchip (at this time owner of Atmel, the original company behind the AVR). Typical one will see the I/O ports have the same (or less) bit count vs. the core. So for these AVRs the I/O port size is 8 bit (or less). If one should use STM32, the port size is typically 32bit wide (and STM32 has the advantage one can single step the code / put breakpoints in the code).

AVR I/O Ports on the Uno and Mega

AVR Ports Vs Arduino Pins.png

Above picture shows the correlation between Arduino Uno (AVR ATMega328) and Arduino Mega (AVR ATMega2560) pin numbering vs. the AVR I/O ports.

Since the aim of this article is to make more efficient code closer to the hardware, the first step is simple: all traffic lights of the same color are connected to same I/O port. In case of the Arduino Uno, this is not possible: in that case the green and orange lights may be combined in one port and the red one in another. Basically the code stays about the same, but one will not have the ability to preserve the GREEN[] ORANGE[] and REG[] arrays. Instead, one has to combine these array data vs. the available & used I/O ports.

In this example, we use an AVR Mega. That controller has several 8-bit ports completely free for the application.

I've chosen:

PORTA = all Green lights,

PORTC = all Orange lights,

PORTL = all Red lights.

Since each port has 8 outputs, we can control 8 lights in parallel with a single write operation.

For ease of use, the 1st traffic light is connected to PA0 (Arduino D22), PC0 (Arduino D37) and PL0 (Arduino D49), the 2nd traffic light is connected to PA1 (Arduino D23), PC1 (Arduino D36) and PL1 (Arduino D48) and so on. The last traffic light uses PA7 (Arduino D29), PC7 (Arduino D30) and PL7 (Arduino D42). Basically any possible combinations can be used but some might make your life a bit harder to determine what output controls what traffic light at a given time.

If one needs more than 8 traffic lights, an additional set of I/O ports can be assigned, but only PortB, PortF and PortK have 8 additional bits available to the user (from then on, less that 8 bits per port are still free).

Define the Port Data and Data Direction Registers

bitValue.jpg

There are 2 important I/O registers to control the output port: the Port Data Direction Register (DDRx) and Port Data Register (PDRx). Depending on the available outputs for that port, one first has to define the data direction register: all bits set to one make the pin output (where special functions are not used - see the datasheet for that). Since we use all 8 bits of the port for output, the data register of each port is 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1 = 255.

At any given time, one determines what the green, orange and red lights have to be + the time this condition must be preserved. That gives a total of 4 arrays: 3 for the LEDs, a 4th for the time. In this example, we want to turn on the 1st, 2nd, 4th and 6th traffic light of a given port. In any controller, the bit numbers start from count zero (at the left). That means, we have to turn on bit 5, 3, 1 and 0. So we sum the value as 2^5 + 2^3 + 2^1 + 2^0 (or 32 + 8 + 2 + 1) = 43.

Since we have 1 port per color, we have to write the 3 "color" ports, each read from a dedicated array element. The 4th array determines the time in ms that state is preserved. When the last stage is put on the ports, the sequence starts from the start.

That brings us to the code: as shown in above screenshot: this is very dense and much easier to maintain. For instance, to convert the lights from Belgian (probably more universal) to German lights (more smart, because they turn orange just before turning green so drivers are aware they have to get ready to start) no code change is required. All one has to do is put some extra data in the arrays and build the project again (one can even put both arrays in and select based on a jumper position that is read only at the start of the sequence or after boot - whatever one wants.

Final Note

software.png

If one uses an Uno, it may be some bits of the I/O port are not free but already used. The best way to avoid one writes the wrong configuration into the data direction register, is to read it into a variable, turn on the bits used by the traffic light by OR-ing and write that back into the data direction register.

That can be accomplished by code (example):

ddrx = DDRB; //Assume we use port B on the Uno: bit 6 and 7 are not available.

ddrx |= 15; //We use only D8, D9, D10 and D11 from the Uno -> 8 + 4 + 2 + 1.

DDRB = ddrx; //Update the DDRB register with only the 4 bits

One can also do it in 1 step as:

DDRB |= 15;

This example shows that decoupling the programmer from the hardware is not always making code easier to understand or easier to maintain. For small projects, it's sometimes better to learn about the controller architecture to make denser and simpler code that is far easier to maintain. Unfortunately xlsx files cannot be uploaded to instructables, otherwise I could have attached my Excel sheet that allowed to fill in the traffic light states and time. That excel file outcome could than be copy/pasted into the ino source directly.

Another tip: for those who like to write binary, that's possible as well: the above example would become

DDRB |= B00001111; //The leading zeroes are not required but make clear it's only changing the lower 4 bits.

Personally I prefer to write that in hexadecimal and would rather write DDRB |= 0x0F. Wikipedia and other sites are available that explain how to convert binary data to decimal / hexadecimal and back.

Note: to control de traffic lights output, similar code can be used. To clear the previous status of the traffic light, perform an bitwise AND where all traffic lights outputs are inverted. For instance, if one uses bits 6, 3, 2, 1 and 0 as traffic light outputs, that means the DDRx register for the traffic light LEDs was ORed with B01001111 (0x4F). To clear all these lights at once, perform a PORTx &= ~0x4F (or PORTx &= ~B01001111;) Next, one can turn on the outputs one wants to turn on by PORTx |= ??

As one can see, the whole traffic light code 4 array declarations; the include can even be removed (that's a leftover from debugging prints). Controlling a total of 24 lights for 8 traffic lights requires in this case ~10 code lines. Expanding to 3 more ports (16 traffic lights) would only required 3 additional code lines and 3 additional array declarations (+ setting the DDRx of those port to output).

Another advantage of writing to ports instead of bits is the fact one can ensure one or more pins go high at exactly the same time as other pins on the port are programmed low. If timing is critical, one can better modify the entire port instead of series of port bits.

The disadvantage is the code is completely linked to the hardware. It cannot be ported to -for instance - an ESP32/ARM based Arduino without some minor changes. But if one has 3x8 traffic lights, this code is much easier to understand vs. a huge sequence of I/O pins being driven high/low individually.