Everything for Electronics

Microcontrollers, Software, and You — Part 3

Microcontrollers, Software, and You — Part 3

By Jack Purdum    View In Digital Edition  


Part 3: Organizing a Program

Our last article asked you to write a program that would accept a number from the user and then display the square of that number. If you haven’t written your sketch yet, I hope you’ll do so before reading any further. As with most things, the way to get better at something is to do it often. It’s no different with programming.

My Program

First, if your program produces the correct answer, that’s wonderful. I hope you used the Five Program Steps as a starting point for your answer. (More on using the Five Program Steps for designing a program in the next article in the series.) Second, while getting the correct answer to a programming problem is crucial, it should not be your only objective. For all but the most trivial of programs, you also want to write it with sufficient clarity that someone else can read your code and easily understand what the code does.

Realize it or not, six months from now, you might become that “someone else” who has to figure out what the program does.

Finally, keep in mind there is likely more than one way (i.e., algorithm) to produce the correct answer.

With that in mind, my sketch to address the problem is presented in Listing 1.

/*****
  Purpose: Displays a message and value for squared number

  Parameter list:
    int sqr

  Return value
    void

  CAUTION: Assumes the Serial object is instantiated
*****/
void ShowSquare(int sqr)
{
  Serial.print(“The square = “);
  Serial.println(sqr);
}

/*****
  Purpose: To accept a number from the keyboard via the Serial object

  Parameter list:
    void

  Return value
    int         the number that was entered


  CAUTION: Assumes the Serial object is instantiated
*****/
int GetNumber()
{
  char buffer[10];
  int count;

  buffer[0] = ‘\0’;
  while (Serial.available() > 0) {
    count = Serial.readBytesUntil(‘\n’, buffer, sizeof(buffer) - 1);
    buffer[count] = ‘\0’;
    Serial.print(“The number to square is “);
    Serial.println(buffer);
  }
  return atoi(buffer);
}

/*****
  Purpose: To accept a number from the keyboard

  Parameter list:
    int num       the number that was entered

  Return value
    int           the square of the number
*****/
int DoSquare(int num)
{
  if (num < 182)      // Number greater than 181 overflows an int
    return num * num;
  else
    return -1;
}
//===============================================
void setup() {
  Serial.begin(9600);    // Step 1, Initialization

  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  Serial.println(“My Square routine”);
}

void loop() {
  int value;
  int square;

  Serial.println(“\nEnter the number to square:”);

  while (true) {
    square = 0;
    value = GetNumber();          // Step 2, Input
    square = DoSquare(value);     // Step 3, Process
    if (square == -1) {
      Serial.println(“Number too large to square”);
    } else {
      if (square > 0) {
        ShowSquare(square);       // Step 4, Output
      }
    }
  }
}

LISTING 1. Program to square a number.


Code Organization

First, notice the organization of the code. The functions I wrote — GetNumber(), DoSquare(), and ShowSquare() — all appear before the setup() and loop() functions where those functions get used. (The process of executing the code within a function is often referred to as “calling a function.”) While the IDE (integrated development environment) compiler is smart enough not to require this organization, placing the code for the functions you write before those functions are used lets the compiler “see” each function’s signature before using it.

You might ask: “So what?” Well, doing this allows the compiler to check to make sure that you’re using the functions return data type correctly (e.g., that you are assigning the number returned by GetNumber() into an int data type, e.g., value). This process is illustrated in the following statement in loop():

    value = GetNumber();          // Step 2, Input

Why is this match between the return data type from GetNumber() and the receiving variable value important? As you know, an int uses two bytes of memory. Now, suppose you change the DoSquare() function so its function type specifier returns a long data type instead of an int. A long data type requires four bytes of memory.

If you try to assign a long data type into an int data type, there could be a problem. You’re trying to pour four bytes of data into a two-byte bucket. Not good, because two bytes of data are going to end up slopping all over the floor.

By placing the function code in the program before it’s called (i.e., used), the compiler can verify that you don’t have a mismatch between the return value from the function and the variable into which you are assigning it. In similar fashion, the compiler can check to make sure you’re passing the proper data type to the function, too.

For example, suppose you changed DoSquare() to accept its input as a long rather than an int (i.e., the parameter is now long num instead of int num in the function signature). Now, suppose you forgot that you’re supposed to send a long to DoSquare() and you pass it an int instead. Doing that is like sending two bytes of valid data to the function, but the function grabs four bytes because the function’s signature tells it you sent it a long. My guess is that squaring four bytes of data that consists of two bytes of “good” data and two more bytes of junk isn’t going to work very well.

Simply stated, placing the function definition code before it’s actually used in the program allows the compiler to perform type checking on the code. Compiler type checking simply means that the compiler is verifying that the data you send to a function and the value returned from the function match their intended data types.

Function Names

Another thing to notice is that I’ve begun each of my function names with an uppercase letter. Truth be told, this is a convention I use but not many other programmers do. So, why make the first letter uppercase?

The reason is because this tells me almost immediately that I was the one who wrote this function. The people who created the Arduino IDE and the supporting libraries always seem to start their function and method names with a lower case letter (e.g., setup(), loop(), atoi(), Serial.print(), Serial.readBytesUntil(), etc.). This means that when I’m testing or debugging a program and I see a function that has an identifier (name) that starts with an uppercase letter, I know that I have the source for that function in the program and that I wrote it. Sometimes that saves me time since I know I have access to the code for that function.

Function Headers and Program Comments

Another convention I use (and most other programmers don’t) is a particular style of function header. I’ve repeated one of the headers in the following code fragment:

/*****
  Purpose: Displays a message and value for squared number

  Parameter list:
    int sqr

  Return value
    void

  CAUTION: Assumes the Serial object is instantiated
*****/
void ShowSquare(int sqr)

First, you need to know that a multi-line comment in C starts with an open comment pair of characters (“/*”) and ends with a closing comment pair (“*/”). Everything between these character pairs is ignored by the compiler. The comment can be one or more lines long. Comments are simply information you want in the program but without bloating up the program’s size. So, why the extra four asterisks before and after the comment characters?

For over 15 years, I had my own software company which produced programming tools (compilers, assemblers, linkers, editors, an IDE) and other programs. By requiring each of my programmers to follow this function header style, each Friday I could run a program that examined every program source code file the programmers had written.

The program looked for the opening header sequence (“/*****”) and upon finding it, the program wrote whatever it read from the next line up to the closing header sequence (“*****/”), plus one more line (which is the function’s signature) to a disk data file. Another program sorted the information by function name and then printed it out.

What I ended up with was a printout that documented every function we wrote for the project. The programmers could then use the document to discover and properly use any new functions we wrote. This function header style is useful because it: 1) tells you what the function does; 2) the type of data that must be sent to the function for it to do its job; 3) the data type that is returned from the function; and 4) any precautions that might be necessary for the function to perform properly.

How good were my programmers at following this style? Pretty good, because any programmer who wrote a function that didn’t conform to this style had to buy beer and pizza for the next Friday’s lunch for all of the programmers.

Think about it ... do you think this caused all the programmers to look for malformed function headers? Usually, one mess up was all that was required for a programmer to faithfully use the style.

You can also use the “//” character pair to introduce a single-line comment.

You can see several examples in Listing 1.

   value = GetNumber();          // Step 2, Input

With single-line comments, everything after the “//” comment pair to the end of that line is ignored by the compiler. As you write more programs, you’ll find yourself using single-line comments to temporarily remove a statement line of code from a program; usually for testing purposes.

You can then remove the comment pair and the line of code is restored in the program. You can use the multi-line comment pairs to temporarily remove larger blocks of code when testing.

The Five Program Steps ... Again

We made the assertion some time ago that virtually every program could be reduced to the Five Program Steps. As we stated before, setup() often corresponds to the Initialization Step. The code in setup() is only executed once and program control immediately transfers to loop(). We have reproduced the loop() code here:

void loop() {
  int value;
  int square;

  Serial.println(“\nEnter the number to square:”);

  while (true) {
    square = 0;
    value = GetNumber();          // Step 2, Input
    square = DoSquare(value);     // Step 3, Process
    if (square == -1) {
      Serial.println(“Number too large to square”);
    } else {
      if (square > 0) {
        ShowSquare(square);       // Step 4, Output
      }
    }
  }
}

The loop() code begins by defining some variables it needs and then uses the Serial object to display an input prompt. That message and the GetNumber() function call serve as Step 2: the Input step.

The GetNumber() function isn’t very robust because we could enter a number too big or too small for the atoi() function to process properly. This is especially bad since I told you before that you should validate the data before using it. (I’ll use the usual academic cop-out: Data validation is left as an exercise for the reader).

Assuming that the number (value) is validated, the next function, DoSquare(), is called. The DoSquare() function performs Step 3: Process. DoSquare() checks to make sure the number passed to it is not large enough to overflow the range of an int.

If the number is too large, the function returns a -1. Because any number squared cannot be negative, we can use the value -1 to indicate some error occurred. For numbers less than 182, the number is squared and returned to the caller.

Note how we check the value of square with an if statement. If square is -1, we display an error message. Otherwise, the function ShowSquare() is called to display the answer on the display. Therefore, ShowSquare() performs Step 4: Output.

Because there are no more statements in loop(), program execution goes back to the first statement in loop() and prompts the user to enter a number. The program continues this sequence until one of the possible termination events (i.e., power removal, component failure, or program reset) takes place.

Assignment

Okay, let’s write a little more complex program. The program is similar to the card game “In Between.” The game is played by dealing two cards from a deck face up. The player then decides if they want to bet that the next card falls “in between” the two cards that are face up.

For example, if the two cards that are face up are a two and a Queen (which has a numeric value of 12), the player makes a bet that the next card falls between these two numbers (i.e., the card is a three up through a Jack (11)). If the third card falls in between the two face-up cards, the player wins the bet. If the card falls outside the range of the high and low cards, the house wins. The house wins ties. I would suggest that when the game starts, the player is given a starting “betting pot” of $100 and your program tracks the balance as the game is played.

You should use the library function named random() to generate the card numbers (Ace = 1, Jack = 11, Queen = 12, and King = 13) used in the game. You can find information about random() at https://www.arduino.cc/reference/en/language/functions/random-numbers/random.

Stop, think, and then write out the Five Program Steps before you start writing any code. Steps 2 through 4 should constitute playing one round. Think about it.

If you can write this program, you’re ready to take on some much more interesting stuff about programming in C.  NV




Comments