Trains.com

Subscriber & Member Login

Login, or register today to interact in our online community, comment on articles, receive our newsletter, manage your account online and more!

Arduino Signals using Sub-functions

2087 views
3 replies
1 rating 2 rating 3 rating 4 rating 5 rating
  • Member since
    July 2009
  • From: lavale, md
  • 4,641 posts
Arduino Signals using Sub-functions
Posted by gregc on Saturday, December 16, 2017 8:18 AM

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

  • Member since
    February 2002
  • From: Reading, PA
  • 30,002 posts
Posted by rrinker on Saturday, December 16, 2017 9:50 AM

 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.

  • Member since
    January 2009
  • From: Bakersfield, CA 93308
  • 6,526 posts
Posted by RR_Mel on Saturday, December 16, 2017 11:16 AM

I’ve spent my entire life in electronics from the early age of 8 until now at 80 and never did anything in programming.  I’m stuck doing it the hard way, there is no way I could take on learning to write code nor do I want to do so.
 
I search the Internet for sketches to modify to my needs and ask for help on the Forum.  And by the way Randy and the rest of you guys thanks a bunch for all your help in the past!
 
I did have success at making my own 14 block signal controller using a MEGA. I tried a lot of sketches searching the Internet but they either wouldn’t compile for me or weren’t what I was looking for.  Most were far too complex for my needs.  I settled for a simple truth table format and it works great for me.  There is probably a much cleaner way of writing the code but it works for me.
 
I overhauled reworked and reinstalled my entire 14 block signal system for under $100.  The $100 includes the cost of fabrication of 28 three color LED search light signal heads, a pair of MEGAs and all of the components needed.  The $100 doesn’t include the IR occupancy detection, that was about an additional $60 before I went with the MEGAs.
 
I do want to thank everyone that helped me get my random lighting controller working right!!!!  I now have 6 of the UNO lighting controllers operating six two story homes on my layout.
 
 
Mel
 
Modeling the early to mid 1950s SP in HO scale since 1951
  
 
My Model Railroad   
 
Bakersfield, California
 
I'm beginning to realize that aging is not for wimps.
 
  • Member since
    January 2010
  • 168 posts
Posted by nycmodel on Sunday, December 17, 2017 10:55 AM

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.

Subscriber & Member Login

Login, or register today to interact in our online community, comment on articles, receive our newsletter, manage your account online and more!

Users Online

Search the Community

ADVERTISEMENT
ADVERTISEMENT
ADVERTISEMENT
Model Railroader Newsletter See all
Sign up for our FREE e-newsletter and get model railroad news in your inbox!