Sunday, December 28, 2014

New Project: Small Traffic Signal

We will use what we have learned up to this point to build a new project, a traffic light.  This project hopes to accomplish a few things:

Look at project planning from the ground up.
Introduce the PIC16F628A.
Use the device datasheet to make project decisions.
Introduce the use of flow-charts for project planning.
Show how changing to a new PIC device does not change everything about how programs are written.

A note about this traffic signal. I understand that in different parts of the world, traffic signals can vary in how they operate. Because I am familiar with traffic signal operations in the United States, this project will be based off standard US signals.

PRE-PROJECT PLANNING

When planning a new project, it is likely that you are working on that project because you want to accomplish something, whether it be control of peripheral features or some timing operation for something.  The very first thing in planning a new project is to write down the goals of your program.  What do you need it to do?  Writing down the needs of your program as a list on paper will help you to itemize your programming tasks.  Figure 1 shows the goals of this traffic signal program.

Figure 1.
Both the main road and the crossing street should have red, yellow, and green lights.  Many intersections in the USA will change to a flashing signal after a certain hour when traffic is less, so I would like this program to contain a flashing "Night Mode" which flashes red to both streets.  The signal should be safe, meaning that the traffic light never shows an unsafe signal, such as green in both directions.  The flashing night mode will be selected using a single-pole single-throw toggle switch connected to an I/O pin.  To keep the part count lower, I am only representing half of a standard signal, which requires 6 LEDs.  A full signal would have three lights for each direction of an intersection, requiring 12 LEDs.  Since we have 6 lights which are independently controlled plus the toggle switch for night mode selection, we need 7 I/O pins.  The PIC12F629 that we've been using up to this point only has 6, so we need to upgrade to a larger device, the PIC16F628A.

Why was the PIC16F628A selected?  Not for one specific reason.  It is a great practice and teaching microcontroller because it has two I/O ports, a wide range of features, an internal oscillator, plenty of program memory space, etc.  You really could use any PIC that has enough I/O ports and the features you need, but I selected this one because I have it in my parts bin, I have used it a lot for my own practice, and they are cheap and easy to get.  At the time of this writing, they are available here for just $2.08USD each.

As programs grow more complex, even slightly so, you will find it beneficial to create a program flow chart (Figure 2).  A program flow chart is used to show how a program proceeds through tasks, and what kinds of decisions the program has to make.  For the traffic signal program, we can see in the flow chart that at the beginning of the program the ports are initialized and then the main loop is entered.  The first task in the main loop is to evaluate whether night mode is selected. Program execution branches here depending on a yes or no response.

Figure 2.
If night mode is not selected, the traffic signal enters standard operation, which starts by displaying a red signal in all directions for 3 seconds before the main road is given a green signal.  You can see that the flow chart depicts fairly standard traffic light operations.  Notice at the bottom of the standard operation flow chart that after the main road red/cross road yellow signal, the program returns to evaluate if night mode is selected.  You might ask why we don't show red in all directions here again.  The answer is that if we programmed red in all directions for 3 seconds here, then the program went to the top and was evaluated no night mode again, we would proceed back to the first standard operation instruction which is red in all directions for 3 seconds.  Having this instruction at the bottom and at the top would result in an all-way red signal for 6 seconds.  Drivers would get impatient.

If night mode is selected at any point, night mode would be evaluated as a yes and a red signal would flash on and off in both directions at a 1/2 second interval.  This is the standard flash rate for flashing signals.  Night mode selection is evaluated again after each on/off cycle.

These flow charts are very useful to understand what we need our programs to do at a glance.  We can look at a block as a task and ask ourselves, how can I accomplish this task?  It makes it easier to look at and write a program one task at a time, rather than trying to think of the whole program.  Trust me, that can be pretty overwhelming.

EVALUATING HARDWARE

We now know what our program is going to do, and what tasks we need to accomplish.  The next step is thinking about hardware.  We need to make the following decisions about our hardware:

What I/O pin will I use for the toggle switch?
What I/O pins will I use for the LEDs?
Can I drive the LEDs directly from the PIC, or will that exceed the voltage/current/power maximums of the device?

To make these decisions we need to reference the device datasheet from Microchip. We will decide what I/O pins to use first.  Figure 3 shows a section of the I/O PORTS chapter (Section 5.0) of the datasheet.  The PIC16F628A device has two I/O port registers designated PORTA and PORTB whose data directions (input or output) are controlled by the TRISA and TRISB registers.  Reading through the I/O PORTS section of the datasheet you will learn that PORTA pin 5 (RA5) is an input only pin, meaning that you cannot output data to it.  We will select this pin as our toggle switch input.  We also learn that PORTB is an 8-bit wide bidirectional port, meaning that PORTB has 8 I/O pins, all of which can be input or output.  We will select RB0 through RB5 for our 6 LEDs.

Figure 3.

We also need to look at the ELECTRICAL SPECIFICATIONS section 17.0 of the datasheet.  Since we have been using LEDs which have a roughly 20mA forward current, we will need to know if the I/O pins can handle it without damage to the device.  Also, since we will have two LEDs on at the same time, we need to know if the PIC can handle upwards of 40mA combined on the I/Os.  The maximum ratings show us that each I/O pin can handle 25mA, and the total current by PORTA and PORTB pins combined is 200mA.  We will be fine driving our LEDs directly from the PIC.

PROJECT SCHEMATIC & HARDWARE

We now have our pins selected.  The PIC16F628A operates at voltages between 3.0 to 5.5V, and as such we will continue to use a 5V linear voltage regulator to supply the device's power.  Our switch for selecting night mode will take pin RA5 high (supply 5V).  Since RA5 will be an input, it is a good idea to tie it to ground with a 10K Ohm resistor.  We will continue to use 220 Ohm 1/4 W resistors for the LEDs, which are tied to ground from the I/O pins through the resistors, into the LEDs and then to ground.  Figure 4 shows our project schematic.

Figure 4.
THE PROGRAM

Since we are using a new PIC microcontroller, I have created a new project in MPLab X.  You may do the same if you like, just be sure to select the PIC16F628A microcontroller as the project device.  The full program is shown in Figure 5.

Figure 5.
Lines 1-20 are a large block of comments that explain what the program is for, and for what microcontroller.

22-24 show an example of sectioning my program so that the code is easier to read.  You will note this format in several locations within the code.

Line 25 shows the use of a preprocessor directive.  This #define simply states the version number of this program.  I had written this program before but made changes to it for this blog, therefore I updated the version to 2.0.  This can be helpful for the programmer for a number of reasons, including if more than 1 person is writing the program, than each user can sequentially update the version number to help keep track of changes.

Continuing with helpful things for the programmer, lines 28-31 show the use of a #if 0 and #endif block.  An if statement only gets executed if the result is true, even in preprocessor directives like #if.  Using #if 0 is a good way to write a large section of comments, or keep a section for updates.  Here I have used the #if 0 for keeping track of known issues, or ideas for revisions.  You can see that I have made a note to maybe try a yellow light cycle for 4 seconds rather than 3.  In a large program, this is a great way to keep track of code bugs and things that you know need updated or fixed.  The #if 0 must always be closed with a #endif.

Line 34 includes the xc.h header file.

Lines 39 and 40 set the configuration word options for the PIC16F628A.  These options can be found in section 14.1 of the device datasheet, or in the chips html file in the xc8 folder of your PC (xc8> docs> chips).  The configuration word bits are set (1) by default, so you must unset (0) them manually if you desire. For those bits that you wish to leave set, there is no need to include them in the #pragma config.

On line 43, we define the oscillator frequency using _XTAL_FREQ.

Lines 44 through 46 show how we can design our own compiler error messages.  If we change our oscillator frequency for some reason to something other than 4MHz, this "#if (_XTAL_FREQ != 4000000)", which means if XTAL_FREQ is NOT EQUAL TO 4000000, will generate an error when we try to compile the program.  In this case, the error is whatever is in the quotation marks following the #error preprocessor directive.  I have asked the compiler to tell me to "check XTAL_FREQ, check timing operations" if the oscillator frequency isn't 4MHz.  This #if is then closed with a #endif.

Lines 50 and 51 define bit states.  I have defined 1 as TRUE and 0 as FALSE.  The preprocessor directive #define TRUE 1 for instance, is used to replace every instance of "TRUE" with "1".  This allows me to write my code in more of a readable English.

Now we get into mapping our Input/Outputs.  First, we tell the program what I/O pin our switch is connected to.  Line 57, which says #define NightMode_Input PORTAbits.RA5 does just that.  PORTAbits.RA5 points the compiler to PORTA, bit 5, and we have named this bit "NightMode_Input".  Later in the program when I type "NightMode_Input", the compiler automatically knows I'm referring to bit 5 of Port A.

We see an example of that in the very next line of code, line 58.  I am defining a bit state here that I have named "NightMode_ON".  NightMode_ON defines that NightMode_Input is TRUE, meaning that bit 5 of Port A is equal to 1.  Now, throughout the rest of the program, if I type "NightMode_ON", the compiler knows I'm saying that bit RA5 is equal to 1.

Line 59 defines NightMode_OFF as bit RA5 being equal to 0.

Lines 62 through 67 define the bit masks for all of our LEDs.  Since we have used bits 0 through 5 of port B for our LEDs, we can mask each one and assign a name to the mask which makes writing the program easier.  For example, the red light of our main road is bit 0, thus our mask for this LED is named "main_REDmask", and bit 0 is given a value of 1 for our bitwise operators later in the program.

Lines 70 through 73 define the initial state of our registers for program start. TRISA, which defines the data direction of port A is set with all bits being outputs except for our switch on bit 5, which is an input.  TRISB is all outputs.  PORTA and PORTB are both cleared for program startup.

Line 78 declares our global variable "PORTBimg" for our shadow register using the 8-bit unsigned char data type.

We now enter the section of code where are functions are declared.  Remember that functions must be declared (defined) before we call them in the program, otherwise the compiler doesn't know what we want and will produce an error. Our lesson on functions talked about passing parameters to and from the functions, and that when we have no parameters to pass the function format is void function_name (void).  This program passes no parameters, so this is the format we use.

The first function on line 84 is "doBOTH_RED_ON".  When we learned about functions, we were turning only one LED on.  We are now dealing with multiple LED combinations to cycle our traffic light, and the doBOTH_RED_ON function turns on the red LEDs for the main road, and the cross road.  Using bitwise operators and the LED masks, the compiler first performs the operation within the parenthesis just like in mathematical equations, then performs the operation outside the parenthesis with the result.  Since we want both red lights on, the first thing that occurs in this function is that both red LED masks are bitwise ORed together inside the parenthesis.  A bitwise OR operation produces a true (1) result when one OR the other bit is 1.  Since both masks have a 1, the result of this OR operation produces an 8-bit result with two bits true (00001001).  Next, the compiler takes this result and ORs it with PORTBimg, which is 00000000.  At the same time, since we have also used the equal operator, PORTBimg is made equal to the result of our OR operation, and PORTBimg now equals 00001001.  The next line of our function copies PORTBimg to the PORTB register, which turns on both of the red LEDs.  Here is a visualization on how this whole operation looks:

main_REDmask is 00000001
cross_REDmask is 00001000

The function says PORTBimg |= (main_REDmask | cross_REDmask)
In binary, this looks like:
00000000 |= (00000001 | 00001000)

The compiler does the OR in parenthesis first.  We now have 00000000 |= 00001001.  This operation makes PORTBimg equal to this result, which is 00001001, and this is then copied to the PORTB register.

The remaining combinations for our traffic signal are represented in the rest of the functions.  To turn off the LEDs, a function was created which bitwise ANDs the PORTB shadow register with the initialization state of PORTB.  PORTBinit is all zeros, so no matter what PORTBimg is currently equal to, ANDing the two together produces a result which is all zeros.  This result is then copied to the PORTB register turning off all the LEDs.

Line 119 is a delay function which is used to generate our long multi-second delays.  This function is named "delayS" to represent delay seconds.  Its format is void delayS (unsigned char S).  It does have a parameter passed into it, which is a local unsigned char variable named S.  S represents the number of seconds we want to delay.  The function is called with a value for S in the program, such as delayS(5);.  The value 5 gets passed into the function, which then creates a for loop.  This loop says that for S, while S is greater than 0, decrement S.  Then a one-second delay macro is used.  If we tried to delay in the program by writing __delay_ms(5000), which would represent a 5 second delay, the compiler would likely generate an error that the 5000 argument is too large.  These macros tend to have size limits, and we would definitely not be able to use this delay macro for a 20 second delay.  Creating a function like this one gets us around those limits by keeping the argument to a small 1 second.  What happens in this function is that the argument "5" in the function call is represented by the variable S.  Then the for loop says for (5; 5>0; 5--), then __delay_ms(1000);.  The function says "okay, 5 is greater than 0, so I will delay 1 second and then subtract 1 from 5."  When this loop has completed 5 times, S is no longer greater than 0 because 0>0 is not true, so the loop stops and the function is exited.

This completes our function declarations, and we now enter into the main program on line 129.  Lines 132 through 136 use our previously defined initialization values to set data direction for TRISA and TRISB, and clear the port registers and shadow register.

We then enter the main while (1) program loop on line 141.  The program loop starts by checking the status of the NightMode_Input.  The program makes a decision on which loop to execute based on whether NightMode is on or off.  If the night mode switch is on, the if loop on line 144 is executed, which flashes the red lights on and off for 500mS each.  If the night mode switch is off, the if loop on line 153 is executed, which follows a normal traffic light operating pattern.  If at any time during program operation the night mode switch is changed, the program will fully complete the current loop before evaluating the switch again and changing loops.  This ensures that modes are changed in a safe and orderly fashion without ever displaying an unsafe signal.  Imagine approaching a traffic signal which is showing a green light, and suddenly without warning it starts flashing red!  This signal program avoids issues like that.

This program demonstrates how the actual program can be comprised almost entirely of function calls.  Functions are very useful.  I really hope this lesson helped you learn a few things.  Thanks for reading.

No comments:

Post a Comment