This is why I'm not really interested in Arduino. I have no desire to write or debug 100s of lines of code.
- BMMENYC
It's one thing to make it work. It's another to make it maintainable.
By maintainable, I mean easy to understand, debug and enhance. It applies to hardware and mechanical design, as well as software. Duplicated information in software is a problem. This includes duplicating code (i.e. cut-and-paste). Any flaw in the original version of the code now needs to be fixed in every place the code was duplicated. Maintainable also means for the developer to be able to look at the code in 6 months and understand how it works.
There are common techniques that improve the maintainability of code. Sub-functions are fundamental. Tables capture behavior in data instead of logic.
A Signaling Example
Signaling is a good example. because the basic concept is simple but it applies to a large number of outputs and interrelated inputs. This example starts with a cut-and-paste approach and moves to more structured and scalable approaches.
The following diagram illustrates just a few block and corresponding signals of a layout. There are three blocks numbered 1-4. The corresponding analog inputs, 1-4, are used to read an active low occupancy detector for each block. Six signals are shown, B-D in the west direction and a-c in the east direction.
<B <C <D ____1_____|____2_____|____3_____|____4_____ a> b> c>
The table indicates the digital output pins controlling the red, yellow and green LEDs of each signal. Setting the pin low turns on the LED. The signal displays red, if the next block is occupied. It displays yellow, if the block after the next is occupied. It displays green, if the next two blocks are unoccupied.
a b c B C D red 0 3 6 10 13 16 yellow 1 4 7 11 14 17 green 2 5 8 12 15 18
The following code fragment is one approach for checking the occupancy of two blocks and setting the signal to either red, yellow or green. A common approach is to copy these 15 lines of code for additional signals. The analog input and digital output pins need to change for each signal. Since each block has signals in both directions, this code would need to be copied twice for each block. There will be over 150 lines of code for ten signals covering five blocks.
// ------------------------------------- // check states for signal c if (analogRead(3) < 500) { digitalWrite(6, LOW); // red digitalWrite(7, HIGH); digitalWrite(8, HIGH); } else if (analogRead(4) < 500) { digitalWrite(6, HIGH); digitalWrite(7, LOW); // yellow digitalWrite(8, HIGH); } else { digitalWrite(6, HIGH); digitalWrite(7, HIGH); digitalWrite(8, LOW); // green }
Hardcoded values for the analog inputs reading the block occupancy and the digital output pins controlling the LED make it tedious to know exactly which blocks are being checked and which signals are being controlled. #defines can provide descriptive text for the analog inputs and digital pins. Creating symbol names with a pattern helps with verification. (The block numbers and pins do not have to be the same).
#define THRESH 500 #define B1 1 #define B2 2 #define B3 3 #define B4 4 #define SIG_a_RD 0 #define SIG_a_YE 1 #define SIG_a_GN 2 // ------------------------------------- // check states for signal c if (analogRead(B3) < THRESH) { digitalWrite(SIG_c_RD, LOW); // red digitalWrite(SIG_c_YE, HIGH); digitalWrite(SIG_c_GN, HIGH); } else if (analogRead(B4) < THRESH) { digitalWrite(SIG_c_RD, HIGH); digitalWrite(SIG_c_YE, LOW); // yellow digitalWrite(SIG_c_GN, HIGH); } else { digitalWrite(SIG_c_RD, HIGH); digitalWrite(SIG_c_YE, HIGH); digitalWrite(SIG_c_GN, LOW); // green }
Sub-Functions
Even this small piece of code is repetitive. The logic for setting a signal can be captured in a sub-function setSig(), passing as arguments the three pins controlling the signal LEDs and the desired signal indication. First it turns off all of the LEDs, then turns on the correct LED. Three new #defines clearly identify the indication being displayed.
#define STOP 1 #define APPROACH 2 #define CLEAR 3 // ------------------------------------------------------------------- void setSig (int rd, int ye, int gn, int indication) { digitalWrite (rd, HIGH); digitalWrite (ye, HIGH); digitalWrite (gn, HIGH);. switch (indication) { case STOP: digitalWrite (rd, LOW); break; case APPROACH: digitalWrite (ye, LOW); break; case CLEAR: digitalWrite (gn, LOW); break; default: Serial.print ("setSig: unknown indication "); Serial.println (indication); } }
Using setSig() simplifies the original code and the arguments make it clearer what it is doing. The original code is reduced to eight lines of code. The same ten signals now requires 80 lines of code, plus the 26 lines for setSig().
// ------------------------------------- // check states for signal c if (analogRead(B3) < THRESH) setSig (SIG_c_RD, SIG_c_YE, SIG_c_GN, STOP); else if (analogRead(B4) < THRESH) setSig (SIG_c_RD, SIG_c_YE, SIG_c_GN, APPROACH); else setSig (SIG_c_RD, SIG_c_YE, SIG_c_GN, CLEAR);
But even these few lines can be simplified with a sub-function. UpdateSig() can determine and update the signal indication given the three signal pins plus the two blocks affecting them.
// ------------------------------------------------------------------- // determine signal indication from block occupancy and set signal void updateSig (int blk, int nextBlk, int rd, int ye, int gn) { if (analogRead(blk) < THRESH) setSig (rd, ye, gn, STOP); else if (analogRead(nextBlk) < THRESH) setSig (rd, ye, gn, APPROACH); else setSig (rd, ye, gn, CLEAR); }
This simplifies the original code down to one line. Those same ten signals now require just 50 lines of code, 40 lines for the two sub-functions and just one additional line for each signal. Two additional signals are shown.
updateSig (B3, B4, SIG_c_RD, SIG_c_YE, SIG_c_GN); updateSig (B2, B3, SIG_b_RD, SIG_b_YE, SIG_b_GN); updateSig (B1, B2, SIG_a_RD, SIG_a_YE, SIG_a_GN);
Tables
Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they'll be obvious. --- Fred Brooks
Another approach demonstrates the use of tables. A table is an array of a C data structures.
The following defines a new C data type, Signal_t, a C structure that is composed of five sub elements: blk, nextBlk, rd, ye and gn. A variable of this type contains these five elements.
typedef struct { int blk; int nextBlk; int rd; int ye; int gn; } Signal_t;
And like all variables, they can be statically initialized (i.e. their initial value set in the code). The following shows an array, sigTbl[], of type, Signal_t, containing three array elements and each array element, which contains five sub-elements, being initialized with the values shown. The #define determines the number of elements in sigTbl[].
Signal_t sigTbl [] = { { B3, B4, SIG_c_RD, SIG_c_YE, SIG_c_GN }, { B2, B3, SIG_b_RD, SIG_b_YE, SIG_b_GN }, { B1, B2, SIG_a_RD, SIG_a_YE, SIG_a_GN }, }; #define SIG_TBL_SIZE (sizeof(sigTbl)/sizeof(Signal_t))
Each table entry contains the arguments to updateSig(). Instead of passing multiple pieces of information to a function, a pointer to the table entry is passed. This same table entry can be passed to setSig(), even though the table entry contains more information than setSig() needs.
Both setSig() and updateSig() are modified; block and LED pin information is passed using a pointer to the table entry containing that information. They show how the sub-elements of the Signal_t data type are references using a pointer (e.g. p->rd);
// ------------------------------------------------------------------- // set LED for specified aspect void setSig (Signal_t* p, int aspect) { digitalWrite (p->rd, HIGH); digitalWrite (p->ye, HIGH); digitalWrite (p->gn, HIGH); switch (aspect) { case STOP: digitalWrite (p->rd, LOW); break; case APPROACH: digitalWrite (p->ye, LOW); break; case CLEAR: digitalWrite (p->gn, LOW); break; default: Serial.print ("setSig: unkhnown aspect "); Serial.println (aspect); } } // ------------------------------------------------------------------- // determine signal aspect from block occupancy and set signal void updateSig (Signal_t* p) { if (analogRead(p->blk) < THRESH) setSig (p, STOP); else if (analogRead(p->nextBlk) < THRESH) setSig (p, APPROACH); else setSig (p, CLEAR); }
The original code, for all possible signals, is minimized to a for loop that calls updateSig() for each table entry.
Signal_t* p = sigTbl; for (int n = 0; n < SIG_TBL_SIZE; n++, p++) updateSig (p);
Any additional signals can be added by adding entries to the table, along with corresponding #defines, without any changes to logic.
Summary
Avoiding hardcoded values and duplicated code makes code easier to write, read and debug. Calls to sub-functions make it clearer what the code is doing instread of requiring the reader to figure it out.
Tables collect separate pieces of related information in one location. Tables make it easier to pass all related information to sub-functions and make it easier to enhance the code with new data and functionality.
The final .ino file containing three signal descriptions is 118 lines, including comments and the diagram. One additional line is needed for each additional signal. No logic is modified to add additional signals, just table entries.
This code can be improved in several ways. A sub-function can be used to control the signal. Adding additional information to a table entry, that sub-function can call separate sub-functions to control different types of LED signals or a semaphore. A sub-function can be used to determine if the block is occupied. It can handle different types of occupancy detectors. It can also handle sidings where occupancy depends on turnout positions.
Using sub-functions and tables can make coding easier. Additional information and techniques can be found on the web.
ask questions
greg - Philadelphia & Reading / Reading
Good post. Those of us used to programming should already know this, but those new to the whole thing may think the only way to do it is the initial example with all that code, and hundreds of lines of code, even if it IS just repetition, is daunting. Using functions like this, especially when you can get them canned from someone else who has done the heavy lifting, turns the job into mostly just a matter of assigning the correct address (variable) to each signal.
--Randy
Modeling the Reading Railroad in the 1950's
Visit my web site at www.readingeastpenn.com for construction updates, DCC Info, and more.
Well done Greg. On an even broader scale many Arduino users incorporate pre-written libraries so we don't have to "sweat the details" for things like servos and LCD displays. Years ago, when I started programming in Visual Basic, I wrote many VB functions to emulate some of the MUMPS operators that I was so familar with in even earlier years. I feel like a caveman even talking about MUMPS. Plus, VB is no longer supported by Microsoft but remains in many legacy apps.