Go Down

Topic: Planning and Implementing an Arduino Program (Read 83293 times) previous topic - next topic

Robin2

Aug 17, 2014, 02:37 pm Last Edit: Aug 17, 2014, 02:44 pm by Robin2 Reason: 1
From what I can see there are many tutorials about doing specific tasks with an Arduino but I have not come across any that guide a newcomer through the process of planning and implementing a program to make it easy to understand and debug.

The following 9 posts in this Thread are an attempt to fill that gap.

...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#1
Aug 17, 2014, 02:38 pm Last Edit: Aug 19, 2014, 09:54 am by Robin2 Reason: 1
CHAPTER 1 - INTRODUCTION

When you are new to programming and the Arduino system it can be very tempting to use one of the example sketches and make changes or add bits onto it to meet your needs.

If what you are doing is even a little bit complex this approach will quickly turn into an incomprehensible mess. There are plenty of questions in the Forum that provide vivid examples of this.

Adding bits and pieces will probably seem fine as long as you have the correct image of the code in your head. But as soon as something doesn't work as expected the mental image will be derailed and figuring out the problem becomes a nightmare - literally - late nights, too much coffee and no progress. More bits are added to try to monitor the progress through the code. And those bits themselves start to cause confusion.

To make matters worse, if you want to get help on the Forum your mess of code will be even less comprehensible to strangers - so you spend more time explaining yourself than getting help.

This Thread shows a simple way of organizing an Arduino project so the code is easy to understand and to debug. And a little bit of organization will actually get your project finished and working more quickly.

The Thread is a series of Lessons that work through all the logic and the steps needed to create the code so that you can follow why it is done that way.

My objective is to write my code that I will understand 12 months later with just a single read-through. Writing code that is easy to understand also makes it much much easier to get assistance on the Forum.


I'm just going to dive straight in and start work on this little project. If you are interested in the philosophy jump to Chapter 9, though it might make more sense after reading Chapter 2.

For these Lessons to make sense we need a moderately complex project. At the same time it should be something that needs few specialized parts so most people can try it. So let's write a program that moves a servo to a position set by a potentiometer, asks the user for input from the Serial Monitor and causes two LEDs to flash at different speeds depending on which of two buttons is pressed.

If you don't have a servo or a potentiometer or LEDS and resistors you can follow along anyway. The program will work perfectly well without those things connected - you just won't see the effect of the code.

I hope the code associated with each Chapter follows properly from the earlier versions but there are 7 versions and some discrepancies may have crept in. I have tested each version on my Uno.

In Reply #14 @JimboZA has very kindly produced a wiring diagram to show all the project connections.

For the more experienced readers
- if you find errors please let me know so I can correct them.
- if you just prefer to do things differently, may I request that you write your own Lessons in another Thread :)

...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#2
Aug 17, 2014, 02:38 pm Last Edit: Aug 18, 2014, 08:47 pm by Robin2 Reason: 1
CHAPTER 2 - ORGANIZATION

To repeat, the plan is to create a program that moves a servo to a position set by a potentiometer, asks the user for input from the Serial Monitor and causes two LEDs to flash at different speeds depending on which of two buttons is pressed.

Every Arduino program must have the functions setup() and loop()

Code: [Select]
void setup() {

}

void loop() {

}


Before we go any further let's try to describe all the "actions" that must happen for this project to work. For the moment don't worry about the order of things - just jot them down as they occur to you. For example ...

     flash LEDa
     flash LEDb
     check the buttons
     set the flashing period
     ask the user for input
     get the user's response
     move the servo
     read the potentiometer
     set the servo position

Without noticing it we now have a huge part of the program designed. Let's make a separate function for each of those actions.

Code: [Select]
void flashLedA() {

}

void flashLedB() {

}

void checkButtons() {

}

void setFlashPeriod() {

}

void askForUserInput() {

}

void getUserResponse() {

}

void moveServo() {

}

void readPotentiometer() {

}

void setServoPosition() {

}


Of course at this stage none of these functions can do anything as they have no code within them - but we'll worry about that later.


Now, how would you go about using all those little functions? Let's try something very simple. What about this ...

Code: [Select]
void setup() {

}

void loop() {
   flashLedA();
   flashLedB();
   checkButtons();
   setFlashPeriod();
   askForUserInput();
   getUserResponse();
   moveServo();
   readPotentiometer();
   setServoPosition();
}


Together with the earlier functions this is a complete Arduino sketch that will compile and run - even if it does nothing.

It is easy to write the code so that the order of the function calls doesn't matter - the Arduino does things so quickly that, for example, it will not be obvious that the change in the led flashing does not happen until the next iteration of loop() after the button is pressed.

Although the order doesn't really matter to the Arduino it will be easier to understand what is going on if we organize the functions in the order that seems sensible to us.

Code: [Select]
void loop() {
   checkButtons();
   setFlashPeriod();
   flashLedA();
   flashLedB();
   
   askForUserInput();
   getUserResponse();
   
   readPotentiometer();
   setServoPosition();
   moveServo();
}

   
I hope you can see that a quick read through loop() gives you a very good overview of what the whole program does.

It is very important to keep in mind that the Arduino will repeat all of these functions hundreds or thousands of times per second. And you must take care to ensure each function takes as little time as possible. The faster loop() can repeat the better.

The complete version of the code at this stage is in LessonA.ino

Code: (LessonA.ino) [Select]
// LessonA.ino
// Initial program with empty functions

void setup() {

}

void loop() {
   checkButtons();
   setFlashPeriod();
   flashLedA();
   flashLedB();
   
   askForUserInput();
   getUserResponse();
   
   readPotentiometer();
   setServoPosition();
   moveServo();
}

void flashLedA() {

}

void flashLedB() {

}

void checkButtons() {

}

void setFlashPeriod() {

}

void askForUserInput() {

}

void getUserResponse() {

}

void moveServo() {

}

void readPotentiometer() {

}

void setServoPosition() {

}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#3
Aug 17, 2014, 02:39 pm Last Edit: Aug 18, 2014, 08:51 pm by Robin2 Reason: 1
CHAPTER 3 - SERVO

Now let's start to add some code so that something actually happens. We will start with the servo as that is easiest.

We need to add a reference to the servo library at the top of the code - before setup(). (Libraries are pre-written code that we use to simplify common tasks).  We need to decide which pin will be used for the servo signal. Because this number won't be changed by the program it can be defined as a constant. And we need to attach the servo to that pin. The new code looks like this

Code: [Select]
#include <Servo.h>

Servo myServo;
const byte servoPin = 5;

void setup() {
 myServo.attach(servoPin);
}


Then we need a variable to record the angle the servo is required to move to. As the range of angles is 0 to 180 a byte (which can hold values from 0 to 255) will be sufficient. Some servos can't move to 0 degrees or to 180 degrees so lets also set min and max angles. Let's start servoPosition at servoMin.

Code: [Select]
const byte servoMin = 20;
const byte servoMax = 160;
byte servoPosition = servoMin;


It is much better to define a named constant (such as servoMin) rather than just include the number (20 in this case) directly in your code. That's because if you find you need to change the number you only need to edit one line of your code rather than remember to change everywhere that you had the number 20.

We will move the servo to the servoPosition when we attach the servo. We also need to include a delay to allow the servo time to move - and to allow us time to see what is happening. We can use the delay() function in setup() because it will only happen once and we won't need anything else to happen at the same time. The delay() function should not be used in loop().

Code: [Select]
myServo.write(servoPosition);
delay(5000);

   
Finally, let's add code to the moveServo() function. All that's needed is

Code: [Select]
myServo.write(servoPosition);

If you have a very small servo with nothing connected to the servo arm (so there is no load on it) you can probably power the servo from the Arduino 5V pin - with the servo signal wire connected to pin 5 (in this case) and the servo GND wire connected to the Arduino GND pin. With a bigger servo there is a risk that it will draw too much current from the Arduino causing it to reset with all sorts of strange consequences. If you think that may be happening give the servo its own power supply but be sure to connect the GND to the Arduino GND.

The full code is in LessonB.ino - try it and see what happens.

Code: (LessonB.ino) [Select]
// LessonB.ino
// Program with some servo activity

#include <Servo.h>

Servo myServo;
const byte servoPin = 5;
const byte servoMin = 20;
const byte servoMax = 160;
byte servoPosition = servoMin;

void setup() {
 myServo.attach(servoPin);
 myServo.write(servoPosition);
 
 delay(5000);
}

void loop() {
   checkButtons();
   setFlashPeriod();
   flashLedA();
   flashLedB();
   
   askForUserInput();
   getUserResponse();
   
   readPotentiometer();
   setServoPosition();
   moveServo();
}

void flashLedA() {

}

void flashLedB() {

}

void checkButtons() {

}

void setFlashPeriod() {

}

void askForUserInput() {

}

void getUserResponse() {

}

void moveServo() {
 myServo.write(servoPosition);
}

void readPotentiometer() {

}

void setServoPosition() {

}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#4
Aug 17, 2014, 02:39 pm Last Edit: Aug 18, 2014, 08:54 pm by Robin2 Reason: 1
CHAPTER 4 - POTENTIOMETER

The next step is to set the servo position with the potentiometer (pot).

We need to decide which Analog pin the center connection of the pot will be connected to. And we need to connect the other 2 connections one each to the Arduino GND and 5V pins. If the servo seems to move the wrong way when you turn the pot just swap the GND and 5v connections.

Ideally the potentiometer should have a resistance of 10,000 (10k) ohms. Don't use one with a resistance less than 1k or more than 20k.

We need a variable to store the reading from the Analog to Digital Converter (ADC). The ADC gives us a 10bit value with a maximum value of 1023 so that won't fit into a byte and we need an int which can hold values from -32,768 to +32,767.


Code: [Select]
const byte potPin = A0;
int potValue;


The code to read the pot value (which will go into readPotentiometer() ) is

Code: [Select]
potValue = analogRead(potPin);

When we turn our pot from end to end the value produced by the ADC will vary from 0 to 1023. However we have assumed the servo only moves between 20 and 160 so we can't use the ADC value directly. The purpose of the setServoPosition() function is to convert the ADC value into a number that can be used by the servo. The range of movement of the servo is 160 - 20 = 140 and the range of pot values is 1023. To illustrate, assume the actual ADC value is 614 (3/5ths of 1023). To calculate the equivalent servo angle we need a little arithmetic. And let's work through the calculation the same way that the Arduino would.

Code: [Select]
servoPosition = (servoMax - servoMin) * (long) potVal / 1023 + servoMin;
                      (    160  -   20    ) *     614      / 1023 +  20
                               140          *     614      / 1023 +  20
                                          85960            / 1023 +  20
                                                     84           +  20
                                                           104

                                                   
Notice that the biggest number in the course of these calculations is 85960. That number won't fit into an int so we needed to add "(long)" to tell the compiler to temporarily use long variables (which can hold values of up to plus or minus 2 billion)
while doing its sums.

Alternatively you could use the map() function - but that would be no fun.


The full code is now in LessonC.ino

Code: (LessonC.ino) [Select]
// LessonC.ino
// Program with some servo activity


//======for the servo==========
#include <Servo.h>
Servo myServo;
const byte servoPin = 5;
const byte servoMin = 20;
const byte servoMax = 160;
byte servoPosition = servoMin;

//======for the potentiometer===
const byte potPin = A0;
int potValue;


void setup() {
 myServo.attach(servoPin);
 myServo.write(servoPosition);
 
 delay(5000);
}

void loop() {
   checkButtons();
   setFlashPeriod();
   flashLedA();
   flashLedB();
   
   askForUserInput();
   getUserResponse();
   
   readPotentiometer();
   setServoPosition();
   moveServo();
}

void flashLedA() {

}

void flashLedB() {

}

void checkButtons() {

}

void setFlashPeriod() {

}

void askForUserInput() {

}

void getUserResponse() {

}

void moveServo() {
 myServo.write(servoPosition);
}

void readPotentiometer() {
 potValue = analogRead(potPin);
}

void setServoPosition() {
 servoPosition = (servoMax - servoMin) * (long) potValue / 1023 + servoMin;
}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#5
Aug 17, 2014, 02:40 pm Last Edit: Aug 18, 2014, 08:57 pm by Robin2 Reason: 1
CHAPTER 5 - LEDs

Let's look at the flashing LEDs next because that will introduce the use of millis() for timing. We will be using the same technique as is used in the Blink Without Delay example sketch that comes with the Arduino IDE. The purpose of using millis() to manage timing (in preference to delay() ) is so that the Arduino can keep doing other things while time moves on. The delay() function holds up everything until it is finished. In our example, it would prevent the potentiometer being read, the servo position being changed and the push buttons being read while the delay() was in progress.

To keep things simple let's assume we want our LEDs to be on for 300 milliseconds and off for a variable time. We will need some variables and constants to keep track. Variables and constants that hold data connected with millis() should be defined as unsigned long. First let's define the constant values.

Code: [Select]
const unsigned long ledOnMillis = 300;
const unsigned long ledAbaseInterval = 500;
const unsigned long ledBbaseInterval = 1000;


We also need a couple of variables to hold the current interval - when the LED is on this will have the value from ledOnMilis and when it is off it will hold the value from ledAbaseInterval (or ledBbaseInterval). This may sound a little complicated but the idea is that a single piece of code can manage the timing for the ON and for the OFF periods.

Code: [Select]
unsigned long ledAinterval = ledAbaseInterval;
unsigned long ledBinterval = ledBbaseInterval;


We also need a couple of variables to record the state of the LED (off or on) (LOW or HIGH).

Code: [Select]
byte ledAstate = HIGH;
byte ledBstate = HIGH;


Using millis() is actually very easy. You record the value when something starts and with every pass through loop() you compare the new value of millis() with the recorded value and when the difference is equal to the required interval you do something. For our flashing LEDs we will record the time in a variable called prevMillis and then the check for the time is like this

Code: [Select]
if (millis() - prevMillis >= interval) {
  // time has expired so do something
}


Note the subtraction. That is essential to get the correct answer even when the value of millis() rolls-over from 2**32 to 0 which it does approximately every 49 days. (Note that in C/C++ 2**32 means 2 to the power of 32. In Excel, for example, this would be written 2^32 - but that means XOR in C/C++.)

We will need two separate variables to keep the timing of our LEDs separate.

Code: [Select]
unsigned long prevLedAMillis;
unsigned long prevLedBMillis;



So that all the timing is based on the same value of millis() we will create another variable which will be updated in every iteration of loop().

Code: [Select]
unsigned long currentMillis;

currentMillis = millis();

   
In order to connect up our LEDs we also need to decide what pins they connect to and set the pins for output. We will use pin 13 and pin 12 simply because pin 13 is the one that connects to the onboard LED in case you don't have any other LEDs.

Always remember to use a resistor (something from 470 ohms to 4700 ohms) in series with all external LEDs so that they don't take too much current and damage the Arduino I/O pins. It's a good idea to have a few LEDs with a resistor soldered to one leg and piece of wire soldered to the other to match the length of the leg with the resistor. Then any time you need an LED you can just push it into the Arduino connectors - the appropriate pin and GND.

Code: [Select]
const byte ledApin = 13;
const byte ledBpin = 12;

 
Code: [Select]
pinMode(ledApin, OUTPUT);
pinMode(ledBpin, OUTPUT);

 
and make sure the LEDs are on to start with - so you know they are working. This technique is often used to demonstrate that all the warning lights work when something is switched on. The delay() that we included in the Servo Chapter will allow us to see that they are on.

Code: [Select]
digitalWrite(ledApin, HIGH);
digitalWrite(ledBpin, HIGH);

 
You may realize that the data for the two LEDs could be stored in an array which would shorten the code a little and remove some duplication. That won't be covered in these chapters.

Now that we have all the variables and constants for our LEDs we can write the code for the function flashLedA().

Code: [Select]
if (currentMillis - prevLedAMillis >= ledAInterval) {
 prevLedAMillis += ledAInterval;
 ledAstate = ! ledAstate;
 if (ledAstate == HIGH) {
   ledAInterval = ledOnMillis;
 }
 else {
   ledAinterval = ledAbaseInterval;
 }
 digitalWrite(ledApin, ledAstate);
}


The code for flashLedB() is identical apart from referring to B rather than A.

Notice the line prevLedAmillis += ledAInterval;  This updates the timing ready for the next interval. Doing it this way rather than using prevLedAmillis = currentMillis; makes the timing more accurate.

At this stage the LEDs will flash but we have no means to change the flashing rate.

The full code is in LessonD.ino

Code: (LessonD.ino) [Select]
// LessonD.ino
// Program with servo, potentiometer and led flashes


//======for the servo==========
#include <Servo.h>
Servo myServo;
const byte servoPin = 5;
const byte servoMin = 20;
const byte servoMax = 160;
byte servoPosition = servoMin;

//======for the potentiometer===
const byte potPin = A0;
int potValue;

//======for the LEDs============
const unsigned long ledOnMillis = 300;
const unsigned long ledAbaseInterval = 500;
const unsigned long ledBbaseInterval = 1000;
unsigned long ledAInterval = ledAbaseInterval;
unsigned long ledBInterval = ledBbaseInterval;
byte ledAstate = HIGH;
byte ledBstate = HIGH;
unsigned long prevLedAMillis;
unsigned long prevLedBMillis;
unsigned long currentMillis;
const byte ledApin = 13;
const byte ledBpin = 12;



void setup() {
 myServo.attach(servoPin);
 myServo.write(servoPosition);
 
 pinMode(ledApin, OUTPUT);
 pinMode(ledBpin, OUTPUT);
 digitalWrite(ledApin, HIGH);
 digitalWrite(ledBpin, HIGH);
 
 delay(5000);
}

void loop() {

   currentMillis = millis();

   checkButtons();
   setFlashPeriod();
   flashLedA();
   flashLedB();
   
   askForUserInput();
   getUserResponse();
   
   readPotentiometer();
   setServoPosition();
   moveServo();
}

void flashLedA() {
 if (currentMillis - prevLedAMillis >= ledAInterval) {
   prevLedAMillis += ledAInterval;
   ledAstate = ! ledAstate;
   if (ledAstate == HIGH) {
     ledAInterval = ledOnMillis;
   }
   else {
     ledAInterval = ledAbaseInterval;
   }
   digitalWrite(ledApin, ledAstate);
 }
}

void flashLedB() {
 if (currentMillis - prevLedBMillis >= ledBInterval) {
   prevLedBMillis += ledBInterval;
   ledBstate = ! ledBstate;
   if (ledBstate == HIGH) {
     ledBInterval = ledOnMillis;
   }
   else {
     ledBInterval = ledBbaseInterval;
   }
   digitalWrite(ledBpin, ledBstate);
 }
}

void checkButtons() {

}

void setFlashPeriod() {

}

void askForUserInput() {

}

void getUserResponse() {

}

void moveServo() {
 myServo.write(servoPosition);
}

void readPotentiometer() {
 potValue = analogRead(potPin);
}

void setServoPosition() {
 servoPosition = (servoMax - servoMin) * (long) potValue / 1023 + servoMin;
}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#6
Aug 17, 2014, 02:40 pm Last Edit: Aug 17, 2014, 02:48 pm by Robin2 Reason: 1
CHAPTER 6 - SWITCH BUTTONS

In this chapter we will add code to read the switch buttons to change the flashing rate. Let's just make Button1 double the flash rate and Button0 put it back to normal. That will be enough to illustrate the concept.

As before, we need to decide what pins will be used for the switch buttons. And we need variables to store the state of the switches

Code: [Select]
const byte button0pin = 8;
const byte button1pin = 9;
byte button0state;
byte button1state;

   

And we need to set the I/O mode for the pins

Code: [Select]
pinMode(button0pin, INPUT_PULLUP);
pinMode(button1pin, INPUT_PULLUP);

   
"INPUT_PULLUP" means that the pin is connected to 5v through an internal pullup resistor. This is the easiest way to use a pin with a switch. The switch just needs to be wired so that when pressed it connects the pin to GND. This also means that when you read the pin with digitalRead(button0pin) it will give a HIGH result when the button is NOT pressed and a LOW result when it is pressed. If you don't have a switch a piece of wire pushed into the appropriate connector can be used - just touch the other end to the GND connection when you want to "press" the switch.

Then we need a little bit of code for the checkButtons() function.

Code: [Select]
button0state = digitalRead(button0in);
button1state = digitalRead(button1in);

   
Now we also need some code for the setFlashPeriod() function. This will use the states of the buttons.

Before we can do this we need to add another pair of variables so we can deal with the changing values of the OFF interval for the LEDs.
   
Up to this point we have just had to worry about 2 values - ledOnMillis and ledAbaseInterval. As the LED was switched on and off one or other of these values was used to time how long it was ON or OFF. Now, however, we want the OFF interval to be changed by our buttons so we need to be able to store the value that has been set by the buttons. We will start off with the base intervals.

Code: [Select]
unsigned long ledAoffMillis = ledAbaseInterval;
unsigned long ledBoffMillis = ledBbaseInterval;

   
This means we need to make a small change to the code in flashLedA() and flashLedB(). Instead of the existing

Code: [Select]
ledAInterval = ledAbaseInterval;

we need to change it to

Code: [Select]
ledAInterval = ledAoffMillis;
   
so that we use the correct value when it is changed by the buttons.

This is the code for the setFlashPeriod() function.

Code: [Select]
if (button0state == LOW && button1state == LOW) {
  return; // if both buttons are pressed do nothing
}

if (button0state == LOW) {
   ledAoffMillis = ledAbaseInterval;
   ledBoffMillis = ledAbaseInterval;
}

if (button1state == LOW) {
   ledAoffMillis = ledAbaseInterval >> 1;
   ledBoffMillis = ledAbaseInterval >> 1;
}

   
Note that >> 1 causes the bits in a number to be shifted one place to the right. This is identical to dividing by 2.

Note that nothing happens here when either of the states is HIGH. HIGH is the normal state when no button is pressed and responding to that would instantly "undo" the effect of pressing a button.

Note that there is no need for extra code to deal with switch-bounce - it is taken care of automatically because the code only responds when the state is LOW.

The full code is in LessonE.ino

Code: (LessonE.ino) [Select]
// LessonE.ino
// Program with servo, potentiometer, led flashes and switch buttons


//======for the servo==========
#include <Servo.h>
Servo myServo;
const byte servoPin = 5;
const byte servoMin = 20;
const byte servoMax = 160;
byte servoPosition = servoMin;

//======for the potentiometer===
const byte potPin = A0;
int potValue;

//======for the LEDs============
const unsigned long ledOnMillis = 300;
const unsigned long ledAbaseInterval = 500;
const unsigned long ledBbaseInterval = 1000;
unsigned long ledAInterval = ledAbaseInterval;
unsigned long ledBInterval = ledBbaseInterval;
byte ledAstate = HIGH;
byte ledBstate = HIGH;
unsigned long prevLedAMillis;
unsigned long prevLedBMillis;
unsigned long currentMillis;
const byte ledApin = 13;
const byte ledBpin = 12;
unsigned long ledAoffMillis = ledAbaseInterval;
unsigned long ledBoffMillis = ledBbaseInterval;

//======for the switch buttons===
const byte button0pin = 8;
const byte button1pin = 9;
byte button0state;
byte button1state;


void setup() {
  myServo.attach(servoPin);
  myServo.write(servoPosition);
 
  pinMode(ledApin, OUTPUT);
  pinMode(ledBpin, OUTPUT);
  digitalWrite(ledApin, HIGH);
  digitalWrite(ledBpin, HIGH);
 
  pinMode(button0pin, INPUT_PULLUP);
  pinMode(button1pin, INPUT_PULLUP);
 
  delay(5000);
}

void loop() {

    currentMillis = millis();

    checkButtons();
    setFlashPeriod();
    flashLedA();
    flashLedB();
   
    askForUserInput();
    getUserResponse();
   
    readPotentiometer();
    setServoPosition();
    moveServo();
}

void flashLedA() {
  if (currentMillis - prevLedAMillis >= ledAInterval) {
    prevLedAMillis += ledAInterval;
    ledAstate = ! ledAstate;
    if (ledAstate == HIGH) {
      ledAInterval = ledOnMillis;
    }
    else {
      ledAInterval = ledAoffMillis;
    }
    digitalWrite(ledApin, ledAstate);
  }
}

void flashLedB() {
  if (currentMillis - prevLedBMillis >= ledBInterval) {
    prevLedBMillis += ledBInterval;
    ledBstate = ! ledBstate;
    if (ledBstate == HIGH) {
      ledBInterval = ledOnMillis;
    }
    else {
      ledBInterval = ledBoffMillis;
    }
    digitalWrite(ledBpin, ledBstate);
  }
}

void checkButtons() {
  button0state = digitalRead(button0pin);
  button1state = digitalRead(button1pin);
}

void setFlashPeriod() {

  if (button0state == LOW && button1state == LOW) {
    return; // if both buttons are pressed do nothing
  }

  if (button0state == LOW) {
     ledAoffMillis = ledAbaseInterval;
     ledBoffMillis = ledAbaseInterval;
  }

  if (button1state == LOW) {
     ledAoffMillis = ledAbaseInterval >> 1;
     ledBoffMillis = ledAbaseInterval >> 1;
  }
}

void askForUserInput() {

}

void getUserResponse() {

}

void moveServo() {
  myServo.write(servoPosition);
}

void readPotentiometer() {
  potValue = analogRead(potPin);
}

void setServoPosition() {
  servoPosition = (servoMax - servoMin) * (long) potValue / 1023 + servoMin;
}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#7
Aug 17, 2014, 02:41 pm Last Edit: Aug 18, 2014, 08:59 pm by Robin2 Reason: 1
CHAPTER 7 - QUESTION FOR USER

Now we will deal with getting information from the user. I have left this to last because we will be using some of the techniques we have already explored. The timing of a user's response is unpredictable - he or she may provide an answer immediately or not until after they have finished a telephone conversation. And we don't want this part of the code to interfere with the rest of things.

The concept here is very basic - just enough to show the principles involved. The Arduino will print a message on the Arduino Serial Monitor and then listen for a reply. While it is listening it will continue flashing the LEDs and controlling the servo.

When the reply comes it will say "Thank you" and repeat the response. It will then wait 5 seconds before asking the question again.

This chapter just deals with asking the question. We'll deal with the response in the next chapter.


We will store our question and the response in standard C/C++ strings (note the small s). A string is simply a char array with a 0 to mark the end of the text. NB this is the number 0, not the character '0' which has a number of 48.

Code: [Select]
const char question[] = "Please type some text and press ENTER";
const byte buffSize = 31;
char userResponse[buffSize];


The compiler will automatically allocate enough space for the text within double quotes followed by a 0.
The array userResponse has space for 30 characters plus a 0.

We need a constant and a variable to manage the time between a response and a question - similar to the way we managed the flashing LEDs.

Code: [Select]
const unsigned long questionInterval = 5000;
unsigned long prevResponseMillis = 0;

   
The code for askForUserInput() has to check if it is the correct time and then send the question. Importantly, it also has to check whether a response has been received so we need another variable to record that.

Code: [Select]
boolean waitingForResponse = false;

By starting with this "false" the question will be asked first time around.

And, of course, we need to start the Serial communication in setup() with

Code: [Select]
Serial.begin(9600);
   
I am using a baudrate of 9600 but you can use any value you wish as long as the Serial Monitor is set to use the same rate.

I find it useful to print the name of the program in case you have forgotten what program is uploaded onto your Arduino. It also shows that the Arduino is working up to that point in the code.

Code: [Select]
Serial.println("Starting LessonF.ino");

This is the code for askForUserInput()

Code: [Select]
if (waitingForResponse == true) {
 return;
}
if (currentMillis - prevResponseMillis >= questionInterval) {
 Serial.println(question);
 waitingForResponse = true;
}

 
If waitingForResponse is already true the function just returns without doing anything.

If waitingForResponse is false and the time has elapsed the question is printed and waitingForResponse is changed to true so we can wait for the response.

Note that, unlike the code for flashing the LEDs, the value of prevResponseMillis is not updated. That will only happen when the response is received.

Note that we could use an else clause as in

Code: [Select]
if (waitingForResponse == true) {
 return;
}
else {
 // the rest of the code
}


but it is unnecessary because of the use of return;.

The full code at this stage is in LessonF.ino

Code: (LessonF.ino) [Select]
// LessonF.ino
// Program with servo, potentiometer, led flashes, switch buttons and question


//======for the servo==========
#include <Servo.h>
Servo myServo;
const byte servoPin = 5;
const byte servoMin = 20;
const byte servoMax = 160;
byte servoPosition = servoMin;

//======for the potentiometer===
const byte potPin = A0;
int potValue;

//======for the LEDs============
const unsigned long ledOnMillis = 300;
const unsigned long ledAbaseInterval = 500;
const unsigned long ledBbaseInterval = 1000;
unsigned long ledAInterval = ledAbaseInterval;
unsigned long ledBInterval = ledBbaseInterval;
byte ledAstate = HIGH;
byte ledBstate = HIGH;
unsigned long prevLedAMillis;
unsigned long prevLedBMillis;
unsigned long currentMillis;
const byte ledApin = 13;
const byte ledBpin = 12;
unsigned long ledAoffMillis = ledAbaseInterval;
unsigned long ledBoffMillis = ledBbaseInterval;

//======for the switch buttons===
const byte button0pin = 8;
const byte button1pin = 9;
byte button0state;
byte button1state;

//======for user question========
const char question[] = "Please type some text and press ENTER";
const unsigned long questionInterval = 5000;
unsigned long prevResponseMillis = 0;
boolean waitingForResponse = false;

//=====for user response=========
const byte buffSize = 31;
char userResponse[buffSize];


void setup() {
 myServo.attach(servoPin);
 myServo.write(servoPosition);
 
 pinMode(ledApin, OUTPUT);
 pinMode(ledBpin, OUTPUT);
 digitalWrite(ledApin, HIGH);
 digitalWrite(ledBpin, HIGH);
 
 pinMode(button0pin, INPUT_PULLUP);
 pinMode(button1pin, INPUT_PULLUP);
 
 Serial.begin(9600);
 Serial.println("Starting LessonF.ino");
 
 delay(5000);
}

void loop() {

   currentMillis = millis();

   checkButtons();
   setFlashPeriod();
   flashLedA();
   flashLedB();
   
   askForUserInput();
   getUserResponse();
   
   readPotentiometer();
   setServoPosition();
   moveServo();
}

void flashLedA() {
 if (currentMillis - prevLedAMillis >= ledAInterval) {
   prevLedAMillis += ledAInterval;
   ledAstate = ! ledAstate;
   if (ledAstate == HIGH) {
     ledAInterval = ledOnMillis;
   }
   else {
     ledAInterval = ledAoffMillis;
   }
   digitalWrite(ledApin, ledAstate);
 }
}

void flashLedB() {
 if (currentMillis - prevLedBMillis >= ledBInterval) {
   prevLedBMillis += ledBInterval;
   ledBstate = ! ledBstate;
   if (ledBstate == HIGH) {
     ledBInterval = ledOnMillis;
   }
   else {
     ledBInterval = ledBoffMillis;
   }
   digitalWrite(ledBpin, ledBstate);
 }
}

void checkButtons() {
 button0state = digitalRead(button0pin);
 button1state = digitalRead(button1pin);
}

void setFlashPeriod() {

 if (button0state == LOW && button1state == LOW) {
   return; // if both buttons are pressed do nothing
 }
 
 if (button0state == LOW) {
    ledAoffMillis = ledAbaseInterval;
    ledBoffMillis = ledAbaseInterval;
 }

 if (button1state == LOW) {
    ledAoffMillis = ledAbaseInterval >> 1;
    ledBoffMillis = ledAbaseInterval >> 1;
 }
}

void askForUserInput() {
 if (waitingForResponse == true) {
   return;
 }
 
 if (currentMillis - prevResponseMillis >= questionInterval) {
   Serial.println(question);
   waitingForResponse = true;
 }
}

void getUserResponse() {

}

void moveServo() {
 myServo.write(servoPosition);
}

void readPotentiometer() {
 potValue = analogRead(potPin);
}

void setServoPosition() {
 servoPosition = (servoMax - servoMin) * (long) potValue / 1023 + servoMin;
}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#8
Aug 17, 2014, 02:42 pm Last Edit: Aug 17, 2014, 02:53 pm by Robin2 Reason: 1
CHAPTER 8 - RESPONSE FROM USER

This chapter deals with getting the response from the user. In the previous chapter we created the variable where we can save the user's response.

It's worth taking a little time to think about the process of receiving Serial data.

Even at 115,200 baud (about 11,000 characters per second) serial data is sent very slowly by Arduino standards so it is not a good idea to have your program hanging around waiting for data if there are other things to do.

Incoming data is stored in the Serial input buffer in the background so we don't need to react as every character arrives. The standard buffer can hold 64 characters. When your code uses Serial.read() it takes a character from the input buffer and makes room for another incoming character. If you don't take data out of the buffer before it fills up all subsequent incoming characters will be lost. This is not likely to be a problem for this project.

For this project we have no control over how many characters the user will send. S/he might send 1 or 20 (or more). How will we know that we have everything?
   
We could keep checking if there is at least one character in the buffer with

Code: [Select]
if(Serial.available() > 0)
   
but we might accidentally check in the interval between two incoming bytes at an instant when the buffer was empty so we would incorrectly think we had everything.

The simplest reliable way to check that we have all the data is to ensure there is a special character - an end-marker - as the last thing that is sent. Obviously the end-marker must be something that would never occur in the body of the response. The code will keep checking for characters until it detects the end marker.

At the bottom of the Arduino Serial Monitor window there is a box which probably says "No line ending". If you change it to "Carriage return" the Serial Monitor with add a carriage-return character ('\r' or 13) after the text the user enters and we can use this as our end-marker.

This all means we need variables to record what we are using as the end-marker and to keep track of how many bytes have been received so far.

Code: [Select]
const char endMarker = '\r';
byte bytesRecvd = 0;


I mentioned earlier that we don't want our getUserResponse() function to hang around waiting for the user to send a response. Instead we want it to make a quick check to see if there is a character in the buffer. If there is it should copy that character to our userResponse[] array. And if the character is a carriage-return it should do whatever is necessary when the full response has arrived.

It is probably easier to explain this process if I show the code for getUserResponse() first. It's a bit longer than the earlier pieces

Code: [Select]
if (waitingForResponse == false) {
 return;
}

if(Serial.available() == 0) {
 return;
}

char inChar = Serial.read();

if (inChar != endMarker) {
 userResponse[bytesRecvd] = inChar;
 bytesRecvd ++;
 if (bytesRecvd == buffSize) {
   bytesRecvd = buffSize - 1;
 }
}
else { // inChar is the endMarker
 waitingForResponse = false;
 userResponse[bytesRecvd] = 0;
 prevResponseMillis = currentMillis;
 // do something now that response is received
 
}

 
The first part should be familiar - it just terminates the function if the code is not waiting for a response.

The second part terminates the function if there is no data in the Serial buffer - so the program is not hanging around wasting time.

The rest of the code only applies if there is at least one character in the Serial buffer.

First it reads one character into the temporary variable inChar.
Then it tests to see if this is the end-marker.

If it is NOT the end-marker the character is added to the userResponse array and the number of bytesRecvd is incremented.

Note the little bit of code to make sure bytesRecvd does not exceed buffSize. If that did happen data would be written all over parts of memory where it has no business being. This is a nice example of something that is unlikely to happen but would make a dreadful mess if it did.


If the received character IS the end-marker, the code changes waitingForResponse to false, adds a 0 in the next character position as the string terminator and updates prevResponseMillis to cause the code to wait before asking the question again.

Note that there is no code here to set the value of bytesRecvd back to 0 ready for the next response from the user. That is just in case other parts of the code might be interested to know how many bytes were actually received.

The best place for the code

Code: [Select]
bytesRecvd = 0;

is at the end of the function askForUserInput() - in other words immediately before the user responds.


Now we need a short digression to deal with something I forgot when setting out the original actions in Chapter 2. I have marked the place in the code with a comment. We need to do something when the response from the user has been received. So let's create a new function for that - and let's call it acknowledgeResponse().

But from where are we going to call that function?

One solution is to replace the comment with a call to the function. However that is not consistent with the general approach of calling all our functions from loop(). Putting the function in loop() ensures that we know it exists just by reading through loop(). So let's assume the function is called from loop().  This is the code for it - all very basic.

Code: [Select]
if (ackResponse == false) {
 return;
}
Serial.println();
Serial.println("Thank you");
Serial.print("You entered the following ");
Serial.print(bytesRecvd);
Serial.println(" bytes ");
Serial.print(" --- ");
Serial.println(userResponse);
Serial.println();

ackResponse = true;


There is a new variable here - ackResponse - which is used to ensure that acknowledgeResponse() only gets run once for every completed response. Although we already have the variable waitingForResponse, it is not suitable because it will continue to be false through several iterations of loop() and we only want this new function to be called once. This variable is set to true when the complete response is received by replacing the comment with

Code: [Select]
ackResponse = true;

and it is immediately set back to false when acknowledgeResponse() has printed its message.


This new acknowledgeResponse() function was not part of the original plan and it nicely illustrates how easy it is to add additional actions with little or no impact on the rest of the code.


The full code is in LessonG.ino - it is now too long to be included directly in this post

...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#9
Aug 17, 2014, 02:42 pm Last Edit: Aug 17, 2014, 02:54 pm by Robin2 Reason: 1
CHAPTER 9 - Conclusions / Musings

I hope I would easily understand what this program does even if I had not seen it for 12 months. And there are very few comments in it.

Using meaningful names for functions and variables makes code much easier to understand.

Some of the functions contain just a single line of code and you may well say that it could have been placed directly in the code in place of the function call. But, for me, the advantage of the function is its meaningful name.

Using functions allowed the general structure of the program to be created very quickly. Even thougg the functions have no code in them they clearly demostrate their intention.

Using functions allows the code to be considered at different levels of abstraction. The highest level is the general outline that is in loop(). Often it is useful to have a general picture in your mind before considering some piece of detail. It helps to understand where the detail fits in the bigger picture.

Using functions allows you to concentrate on the detail for a single action without being confused by code for other actions.

Using functions allows the different actions to be compartmentalised so that it is easy to see what depends on what. If the code in a function does not work you know that there is no need to look for a solution in a different place.

Using functions allows the different functions to be copied easily and used elsewhere. If you are unsure how something works (let's pretend it is your first time with a servo) you can try the moveServo() function in a short separate sketch while you play around with the different options. And functions caan easily be copied into another project to save a lot of re-writing.

Using functions allows new pieces to be added easily - as we saw with the acknowldegeResponse() function that had been overlooked in the original design.

Using functions does not necessarily mean slower code. In many cases the Arduino compiler creates inline code rather than maintaining the function structure of the source code.

I have created almost all the variables and constant as globals. It simplifies the code and I think it is perfectly reasonable on an Arduino with very limited memory and no operating system. The wholesale use of global variables would almost certainly not be a good idea in a program on the PC.

Enjoy or ignore as it pleases you.

...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Robin2

#10
Aug 17, 2014, 02:43 pm Last Edit: Aug 17, 2014, 02:56 pm by Robin2 Reason: 1
This is the full code for LessonG.ino (which would not fit at the end of Chapter 8)

Code: [Select]
// LessonG.ino
// Program with servo, potentiometer, led flashes, switch buttons, question and response


//======for the servo==========
#include <Servo.h>
Servo myServo;
const byte servoPin = 5;
const byte servoMin = 20;
const byte servoMax = 160;
byte servoPosition = servoMin;

//======for the potentiometer===
const byte potPin = A0;
int potValue;

//======for the LEDs============
const unsigned long ledOnMillis = 300;
const unsigned long ledAbaseInterval = 500;
const unsigned long ledBbaseInterval = 1000;
unsigned long ledAInterval = ledAbaseInterval;
unsigned long ledBInterval = ledBbaseInterval;
byte ledAstate = HIGH;
byte ledBstate = HIGH;
unsigned long prevLedAMillis;
unsigned long prevLedBMillis;
unsigned long currentMillis;
const byte ledApin = 13;
const byte ledBpin = 12;
unsigned long ledAoffMillis = ledAbaseInterval;
unsigned long ledBoffMillis = ledBbaseInterval;


//======for the switch buttons===
const byte button0pin = 8;
const byte button1pin = 9;
byte button0state;
byte button1state;

//======for user question========
const char question[] = "Please type some text and press ENTER";
const unsigned long questionInterval = 5000;
unsigned long prevResponseMillis = 0;
boolean waitingForResponse = false;

//=====for user response=========
const byte buffSize = 31;
char userResponse[buffSize];
const char endMarker = '\r';
byte bytesRecvd = 0;
boolean ackResponse = false;


void setup() {
  myServo.attach(servoPin);
  myServo.write(servoPosition);
 
  pinMode(ledApin, OUTPUT);
  pinMode(ledBpin, OUTPUT);
  digitalWrite(ledApin, HIGH);
  digitalWrite(ledBpin, HIGH);
 
  pinMode(button0pin, INPUT_PULLUP);
  pinMode(button1pin, INPUT_PULLUP);
 
  Serial.begin(9600);
  Serial.println("Starting LessonG.ino");
 
  delay(5000);
}

void loop() {

    currentMillis = millis();

    checkButtons();
    setFlashPeriod();
    flashLedA();
    flashLedB();
   
    askForUserInput();
    getUserResponse();
    acknowledgeResponse();
   
    readPotentiometer();
    setServoPosition();
    moveServo();
}

void flashLedA() {
  if (currentMillis - prevLedAMillis >= ledAInterval) {
    prevLedAMillis += ledAInterval;
    ledAstate = ! ledAstate;
    if (ledAstate == HIGH) {
      ledAInterval = ledOnMillis;
    }
    else {
      ledAInterval = ledAoffMillis;
    }
    digitalWrite(ledApin, ledAstate);
  }
}

void flashLedB() {
  if (currentMillis - prevLedBMillis >= ledBInterval) {
    prevLedBMillis += ledBInterval;
    ledBstate = ! ledBstate;
    if (ledBstate == HIGH) {
      ledBInterval = ledOnMillis;
    }
    else {
      ledBInterval = ledBoffMillis;
    }
    digitalWrite(ledBpin, ledBstate);
  }
}

void checkButtons() {
  button0state = digitalRead(button0pin);
  button1state = digitalRead(button1pin);
}

void setFlashPeriod() {

  if (button0state == LOW && button1state == LOW) {
    return; // if both buttons are pressed do nothing
  }

  if (button0state == LOW) { // LOW means button is pressed
     ledAoffMillis = ledAbaseInterval;
     ledBoffMillis = ledAbaseInterval;
  }

  if (button1state == LOW) {
     ledAoffMillis = ledAbaseInterval >> 1;
     ledBoffMillis = ledAbaseInterval >> 1;
  }
}

void askForUserInput() {
  if (waitingForResponse == true) {
    return;
  }
 
  if (currentMillis - prevResponseMillis >= questionInterval) {
    Serial.println(question);
    waitingForResponse = true;
    bytesRecvd = 0;
  }
}

void getUserResponse() {
  if (waitingForResponse == false) {
    return;
  }

  if(Serial.available() == 0) {
    return;
  }

  char inChar = Serial.read();
   
  if (inChar != endMarker) {
    userResponse[bytesRecvd] = inChar;
    bytesRecvd ++;
    if (bytesRecvd == buffSize) {
      bytesRecvd = buffSize - 1;
    }
  }
  else { // inChar is the endMarker
    waitingForResponse = false;
    userResponse[bytesRecvd] = 0;
    prevResponseMillis = currentMillis;
    ackResponse = true;
   
  }
}

void moveServo() {
  myServo.write(servoPosition);
}

void readPotentiometer() {
  potValue = analogRead(potPin);
}

void setServoPosition() {
  servoPosition = (servoMax - servoMin) * (long) potValue / 1023 + servoMin;
}

void acknowledgeResponse() {
  if (ackResponse == false) {
    return;
  }
  Serial.println();
  Serial.println("Thank you");
  Serial.print("You entered the following ");
  Serial.print(bytesRecvd);
  Serial.println(" bytes ");
  Serial.print(" --- ");
  Serial.println(userResponse);
  Serial.println();
 
  ackResponse = false;
}


...R
Two or three hours spent thinking and reading documentation solves most programming problems.

Hackscribble

Hi Robin

Only spotted one (minor) error on first read through :)

That plus some suggestions for you to consider in attached document.

Regards

Ray
Hackscribble.  Writing about making things
hackscribble@outlook.com

econjack

Robin2: Very helpful. In the organization element, after over 30 years of teaching university-level programming courses, to me the design part is where most students drop the ball. Agile technology suggests the planning doesn't have to be elaborate, like the old IBM BDUF (Big Design Up Front) approach. As a result, I use the Five Programming Steps (details in Beginning C for Arduino and several other of my books):

   Step 1. Initialization. Sets the environment for running the program. Opens ports, DB connections,
         file handles, memory allocations, etc. Like many Windows programs that update the File menu
         with the most-recently used files, this step gets things ready. Done before the user "sees" anything.
         For the Arduino, setup() seems appropriate.

   Step 2. Input. Gather the data that will be used by the program. Verify its validity if possible and react
         if not as expected. Usually means reading sensors, pots, ports...whatever. This is usually an early
         segment of the loop() code. Using function calls helps to encapsulate the code.

   Step 3. Processing. Virtually all programs take data in one form, crunch on it, and output it in another
         form. The "crunching" is done in this step. In most cases, an algorithm directs the processing.
         Sometimes multiple processing steps must be done to interact and produce a given result. This is
         also part of the loop() code, but Steps 2 and 3 isolate each other by using function calls.

   Step 4. Output. This is where the results of Step 3 are presented. The output may be displayed on LEDs,
         LCD display, sensors, meters, printers, data files, or passed on to anther process where it becomes
         Step 2 for that process. Again, a function call should be used to decouple the output from the other
         steps as much as possible.

   Step 5. Termination. This is where most programs clean up after themselves...freeing memory allocations,
         closing DB connections, closing ports, turning sensors off, releasing file handles, etc. In many cases, it
         "undoes" what Step 1 did. For microcontroller applications, most do not have a Step 5 as they are often
         designed to run forever, or until power is removed or there is component failure.

Beginning students rarely take the time to think through a simple set of design steps. Taking these Five Steps and doing a sideways refinement often saves a ton of time down the road.

Robin2


That plus some suggestions for you to consider in attached document.


Thanks Ray, for taking so much trouble with the comments. I will probably include them all - but perhaps tomorrow.

...R
Two or three hours spent thinking and reading documentation solves most programming problems.

JimboZA

#14
Aug 17, 2014, 07:14 pm Last Edit: Aug 24, 2014, 02:36 pm by JimboZA Reason: 1
Nice work Robin.

I've taken the liberty of doing a schematic in Eagle ExpressSCH. Both the .sch and a .bmp are attached hereto.
Jim

Revision history:
V2: Notes added
V3: POT wiring re-routed for clarity
V1.3: Re-numbered as Major.minor
V1.4: Re-issued in ExpressSCH

Apologies for largish .bmp file. Any smaller and clarity is an issue, so best to download and view outside of the forum.

Thanks to TomGeorge for the Arduino Uno library component.



Johannesburg hams call me: ZS6JMB on Highveld rep 145.7875 (-600 & 88.5 tone)
Dr Perry Cox: "Help me to help you, help me to help you...."
Your answer may already be here: https://forum.arduino.cc/index.php?topic=384198.0

Go Up