Chapter 12: Programming, Debugging and Testing in Microcontrollers/SBC

On 20 July 1969, with the lunar module a few minutes from the Moon’s surface, a number appeared on the small display in front of the crew: 1202. Neither astronaut knew what it meant. Seconds later it came again, and then a second code, 1201.

The guidance computer aboard that spacecraft was slower and smaller than the board on your workbench. It was being asked to do too much at once. A radar had been left switched to a setting that kept interrupting the computer with readings nobody needed, and each interruption stole a slice of the machine’s time. The computer was running out of room to finish its work.

What saved the landing was the way the program had been written. Every task inside it carried a priority. When the machine ran short of time, it did not freeze and it did not crash. It threw away the tasks that could wait and kept the ones that were flying the spacecraft — reading the descent radar, steering the engine, tracking the fall. On the ground, an engineer with a handwritten list of alarm codes recognised what the number meant and called out that it was safe to continue.

Fig. 12.0-1 shows the display and keypad through which that computer was watched and commanded. Behind it sat something you are about to learn to write: a plan, worked out in ordinary words long before it became code, that knew what mattered and what could be dropped.

Photograph

Fig. 12.0-1 – the Apollo Guidance Computer’s display and keyboard unit (the DSKY), photographed straight on so that the numeric display panel, the status and caution lights, and the full keypad are all legible in the frame. Sourcing note: a NASA or museum archive photograph of the real hardware unit; artist’s impressions, replicas built for film, and screen captures from simulator software are unusable.

NoteCuriosity Corner

Q1. That radar kept pulling the computer away from what it was doing. What lets a program drop everything the instant an event occurs, instead of waiting until it gets round to checking? Q2. A robot’s controller can only run one instruction at a time, yet it must watch a sensor and keep its wheels turning together. How is that managed without one job blocking the other? Q3. Long before a line of that program was written, the steps were set out in plain language and drawn as a chart. Why is that not wasted effort? Q4. A program is running inside a sealed board with no screen of its own. How can you find out what it is thinking while it runs? Q5. A machine must act only when the system is switched on and an obstacle is close — or when either of two emergency switches is pressed. How are several conditions like these folded into one decision?

This chapter will answer all five of these questions.

12.1 From Problem to Working Robot

A robot is a machine that does nothing at all until it is told what to do. Left on the bench with its battery connected, a wheeled chassis with a sensor bolted to the front will sit still and stay still. The wheels can turn, the sensor can measure, but nothing joins the two.

The sentence “the robot should stop before it hits the wall” is a wish, not an instruction. Somewhere between that wish and a machine that actually stops, a great deal of precise thinking has to happen. This section is about that gap, and about the orderly way engineers cross it.

12.1.1 What a Controller Must Be Told

The board at the centre of a robot — a microcontroller such as the Arduino UNO, or a single-board computer such as the Raspberry Pi — carries out instructions one at a time, in the order they are written, exactly as they are written. It has no judgement and no idea of what you meant. If an instruction says to drive a pin high, the pin goes high, whether or not that was sensible.

The set of those instructions, stored in the board’s memory and executed whenever power is applied, is called a program.

NoteDefinition

A program is an ordered set of instructions, stored in a controller’s memory, that the controller carries out one at a time to produce a required behaviour.

Three quite different kinds of instruction have to be supplied before a robot works at all.

The first kind is configuration: telling the board what each of its pins is for. A pin that will read a switch must be set as an input; a pin that will drive an indicator must be set as an output. The board cannot work this out by looking at what is connected, because electrically it has no way of looking.

The second kind is initialisation: the work that is done once, at switch-on, and never again. Starting the serial link so that the board can report to a computer, setting a motor to a known stopped state, and moving a servo to its resting angle all belong here.

The third kind is the repeating work: reading the sensors, deciding, and driving the outputs, over and over, for as long as power lasts. Almost everything a robot appears to “do” comes from this third part running many thousands of times a second.

Q. What happens if you forget to declare a pin as an output and then write to it?

A. On an Arduino UNO every pin starts as an input at reset. Writing HIGH to a pin still configured as an input does not drive it; it switches on the internal pull-up resistor of roughly 20 kΩ to 50 kΩ instead. Through that resistor an LED would receive well under a milliampere, so it stays dark or barely glows. Nothing is damaged, the sketch compiles, and the fault looks exactly like a broken LED.

Why exactness is not fussiness

In an essay, a small slip of wording is a matter of style. In a robot program it is not. A comparison written with the wrong sign sends a machine forward when it should reverse. A pin number typed as 9 instead of 10 leaves a motor dead while the code that drives it runs perfectly. The board will not warn you, because from its point of view nothing has gone wrong.

This is the reason the rest of the chapter insists so heavily on working out the method before writing the code. The cost of a mistake in a robot is not a lower mark; it is a machine that behaves confidently and wrongly.

12.1.2 Sensing, Deciding and Acting Inside a Program

Whatever a robot does, its program has the same shape underneath. It senses the world, decides what that means, acts on the decision, and then senses again. This cycle is called the sense–plan–act loop: sensing gathers the facts, planning turns the facts into a choice, and acting carries the choice out through an actuator.

Fig. 12.1-1 traces one turn of that loop through the actual hardware, from the physical quantity being measured all the way back to it.

Diagram

Fig. 12.1-1 – flow: physical quantity in the environment → sensor → voltage on an input pin of the controller → reading taken by the program → decision made by the program → signal on an output pin → motor driver → motor → movement, with the last arrow returning to “physical quantity in the environment” to close the loop; each arrow labelled with what travels along it (physical effect, voltage, number, decision, control signal, current, motion)

Two things in that figure deserve attention. First, the controller never touches the world directly. What reaches it is a voltage on a pin, and what leaves it is a voltage on a pin; the sensor and the driver do the translating at each end. Second, the loop is closed. The robot’s own action changes what the sensor will read next, which is exactly why the reading must be taken freshly on every turn rather than once at the start.

Digital and analogue at a pin

Two words are worth pinning down before the programming sections use them constantly. A digital signal has only two meaningful states — a voltage near the supply, read as HIGH, or a voltage near zero, read as LOW. A switch, a touch sensor and a simple infrared line sensor all report this way.

An analogue signal can take any voltage between those limits, and its exact value carries the information. A light-dependent resistor in a divider, a soil moisture probe and a potentiometer all report this way. Such a signal must be converted into a number before a program can compare it with anything, and Section 12.5 shows how that conversion is asked for and what its result means.

12.1.3 The Stages from Idea to Running Code

Experienced engineers do not begin by typing code, and neither should you. The work goes through the stages set out in Table 12.1, and each stage answers a different question. Skipping one does not save time; it moves the difficulty to a later stage where it costs far more to find.

Table 12.1 – the stages between a stated problem and a working robot, showing what each stage produces and the question it settles | Stage | What it produces | The question it settles | |—|—|—| | Statement of the problem | a written specification | What exactly must this machine do? | | Method in words | an ordered list of steps | How will the job be done? | | Method as a picture | a chart of steps and decisions | Where does control go at each choice? | | Method line by line | a draft that is close to code but tied to no language | What will each instruction say? | | Program | code that compiles and uploads | Will the board accept and run it? | | Test and correction | measured behaviour, and fixes | Does it do what was specified, and if not, why? |

The three middle stages have proper names, and Sections 12.2, 12.3 and 12.4 take them in turn.

Writing a specification you can check

Everything rests on the first row. A specification is a statement of required behaviour written precisely enough that someone else could test the finished machine against it without asking you a single question.

“Make the robot avoid obstacles” fails that test. Avoid how? Detected at what distance? Doing what instead?

“When the ultrasonic sensor reports a clear path beyond 20 cm, both wheels turn forward at the same speed. When it reports 20 cm or less, both wheels stop within one sensing cycle, and the robot then turns to the right for a fixed interval before moving forward again” passes it. Every quantity in that sentence is measurable, so every part of it can be checked on a bench with a metre rule.

Notice what a good specification names: what is sensed, what threshold separates one case from the next, what is actuated, and what happens at the boundary. Those four things are precisely what the method in the next section will have to supply.

Beginning with the code What students do: open the editor and start typing a sketch as soon as the task is announced, working out what the robot should do while writing the instructions that do it. What happens: the sketch usually compiles, the robot moves, and nobody can say whether it is right, because nothing was written down for it to be right about. Every later change reopens the same argument. The fix: write the specification first, in four lines if that is all it needs. It costs a few minutes and it is what every later stage is checked against.

Q3. Long before a line of that program was written, the steps were set out in plain language and drawn as a chart. Why is that not wasted effort?

A. Because a mistake caught in a written method costs a rubbed-out line, while the same mistake caught in a running machine costs a rebuilt circuit, a re-uploaded sketch and a long hunt for the cause. Setting the steps out in ordinary words forces every decision, every threshold and every boundary case into the open while they are still cheap to change, and it produces something the finished machine can be judged against. Code written without that stage cannot be wrong, in a useful sense, because there is nothing it was supposed to be.

12.2 Algorithms

12.2.1 What an Algorithm Is

Think of the instructions someone gives you for reaching an unfamiliar building. Walk to the end of this road. If the gate is open, go through it; if it is shut, turn left and use the side entrance. Climb to the second floor. Room 12 is the third door on the right.

Those instructions have a shape worth noticing. They are in a fixed order, each one is a single action, none of them can be read two ways, and following them ends with you somewhere definite. That is exactly what an algorithm is, and the fact that it is written in ordinary English rather than in code changes nothing about it.

NoteDefinition

An algorithm is a finite, ordered sequence of unambiguous steps which, carried out on given inputs, produces the required result or behaviour.

In robotics the inputs come from sensors and the result is a set of actions at the actuators. The algorithm is the piece of thinking that joins them: it converts what the machine has measured into what the machine does about it.

An algorithm is not tied to any programming language. The same eight steps can be written into embedded C for a microcontroller, into Python for a single-board computer, or handed to another student to code. This independence is the whole point. It lets the method be argued about, corrected and agreed before anyone has to worry about semicolons.

12.2.2 The Properties of a Usable Algorithm

Not every set of instructions qualifies. Five properties separate an algorithm from a vague description, and each of them fails in a recognisable way when it is missing.

Finiteness requires that the sequence come to an end after a definite number of steps. Definiteness requires that every step have exactly one meaning. The remaining three concern what goes in, what comes out, and whether the machine can actually perform each step.

Table 12.2 – the five properties of a usable algorithm, with the demand each makes and the failure that follows when it is missing | Property | What it demands | What goes wrong without it | |—|—|—| | Finiteness | The sequence ends after a definite number of steps | The method never reaches an action; the robot waits forever | | Definiteness | Each step has exactly one possible meaning | Two people code it differently, and neither is provably wrong | | Input | The data the method works on is named, with its source | A step depends on a quantity nobody has measured | | Output | The result is named, as a value or as an action | The method produces thinking, but nothing moves | | Effectiveness | Every step is simple enough to be carried out with the resources available | A step such as “identify the object” cannot be executed by the board |

The finiteness property causes real confusion in robotics, because a robot’s program is deliberately built to run for as long as power is supplied. That looks like an endless loop, and endless loops are supposed to be forbidden.

The confusion disappears once you see where finiteness applies. It applies to one pass. Each single journey through the method — read the sensor, decide, drive the motors — must finish in a definite number of steps. Repeating that finished pass, on purpose, for as long as the machine is switched on is a design decision, not a failure. What finiteness rules out is a pass that gets stuck inside itself and never reaches its own end.

Q. A line-following robot’s program runs from the moment it is switched on until its battery dies. Does that break the finiteness property?

A. No. Each pass through the method finishes: the sensors are read, one branch is chosen, the motors are driven, and the pass ends. The perpetual behaviour comes from repeating a finite pass, which is intended. Finiteness would be broken by something quite different — for example, a step that waits for an echo pulse with no time limit, which never ends if no echo ever arrives.

Effectiveness is the property students most often lose, usually by writing a step that describes a wish rather than an operation. “Check whether the path is safe” is a wish. “Measure the distance ahead in centimetres and compare it with 20” is an operation, because the board has a sensor that yields a number and an instruction that compares numbers.

12.2.3 Writing an Algorithm for a Robotic Task

Take the specification written at the end of Section 12.1 and turn it into a method. The machine is a small wheeled trolley carrying an ultrasonic sensor at the front, of the kind sketched in Fig. 12.2-1, and it is required to run forward down an aisle and turn away from whatever it meets.

Diagram

Fig. 12.2-1 – top view of a two-wheeled trolley in a warehouse aisle; the ultrasonic sensor at the front of the chassis drawn with a shaded cone spreading forward towards a crate; the distance from sensor face to crate marked 20 cm with a dimension line; a curved arrow at the chassis showing the on-the-spot right turn; the left and right drive wheels labelled

Read the steps below before reading the code in any later section. This is the order the work is done in, and it is the order the chapter follows.

Stop-and-turn obstacle response for a wheeled trolley Input: the distance to the nearest object directly ahead, in centimetres, measured by an ultrasonic sensor mounted at the front of the chassis. Output: drive signals to the left and right motor channels — both forward, both stopped, or opposed for an on-the-spot turn. Method:

1. Configure the sensor pins and the two motor channels.
2. Stop both motors.
3. Repeat steps 4 to 9 for as long as the trolley is powered.
4. Measure the distance to the nearest object ahead, in centimetres.
5. If no echo returns before the time limit, take the distance as beyond range.
6. If the distance is greater than 20, drive both motors forward at equal
   speed, and return to step 4.
7. Stop both motors.
8. Drive the left motor forward and the right motor backward for a fixed
   interval, so that the trolley turns to the right on the spot.
9. Stop both motors, and return to step 4.

Why it works: forward motion is granted only by step 6, and only while the measured distance is strictly greater than the threshold. Any reading at or below 20 cm therefore removes drive before the trolley is commanded to move again. The turn in step 8 is a fixed action rather than a measured one, and step 9 sends control straight back to a fresh measurement, so the trolley never assumes that the turn has cleared the path. Watch out: a soft, angled or fabric-covered surface may scatter the pulse away and return no echo, which step 5 will read as open aisle. A common ultrasonic module also has a minimum range of roughly 2 cm, below which its readings cannot be trusted. And because step 8 is timed rather than measured, the angle actually turned varies with battery voltage and floor grip.

Notice how much of the thinking happened before any code existed: the threshold, the direction of the turn, the meaning of a missing echo, and the decision to re-measure rather than trust the turn. Every one of those is a design choice that a compiler would never have asked you to make.

A different task reshapes the method rather than merely renaming it. If the same trolley had to stop after passing five floor markers, the method would need a counter that starts at zero, increases by one each time a marker appears, and is tested against five on every pass. The sensing and driving steps would barely change; the structure of the method would.

12.2.4 What Algorithms Do for a Robot

It is tempting to treat the algorithm as paperwork that precedes the real work. In fact four properties of the finished machine are settled here, and nowhere else.

Decision-making. The algorithm fixes what the robot treats as a reason to act. Twenty centimetres rather than ten, “greater than” rather than “greater than or equal to” — these are decisions about behaviour, made in words, long before they become symbols.

Control flow. The algorithm fixes the order of operations and so prevents commands that conflict. Stopping the motors before reversing one of them, as step 7 does, is not decoration; driving both directions of a motor channel at once is exactly the kind of clash an unordered method allows.

Error handling. Step 5 exists because a sensor sometimes returns nothing. A method that assumes every reading is valid produces a robot that behaves strangely and unpredictably in precisely the situations that matter.

Efficiency. How often the sensors are read, how long the machine waits, and how much work is repeated all follow from the method. They decide battery life, and they decide something more immediate: how far the robot travels between one look and the next.

How far a trolley moves between two sensor readings

Given: one full pass through the method takes \(t = 60\) ms; the trolley travels forward at \(v = 0.25\) m/s; the stopping threshold is 20 cm.

Find: the distance covered between consecutive measurements, and the closest the trolley may come to an obstacle before drive is removed.

Method: over a short interval at steady speed, distance is \(d = v \times t\). The worst case is an obstacle appearing just after a reading is taken, so it goes unnoticed for one whole pass.

\[t = 60 \text{ ms} = 0.060 \text{ s}\]

\[d = v \times t = 0.25 \text{ m/s} \times 0.060 \text{ s} = 0.015 \text{ m} = 1.5 \text{ cm}\]

Worst-case clearance when drive is removed \(= 20 \text{ cm} - 1.5 \text{ cm} = 18.5 \text{ cm}\).

the trolley advances about 1.5 cm between readings, and drive is removed with roughly 18.5 cm still to spare, before any coasting.

Check: 0.25 m/s is a slow walk, and 60 ms is about a fifteenth of a second, so a movement of a finger’s width is the right order of magnitude. Note also what the figure ignores: the trolley does not stop dead when drive is removed, so the true clearance is smaller by whatever distance it coasts.

The number matters. Double the speed, or let one careless instruction stretch the pass to 300 ms, and the same method gives a trolley that arrives 7.5 cm closer than intended. Sections 12.7 and 12.8 are largely about keeping that pass short and predictable.

12.3 Flowcharts

12.3.1 What a Flowchart Is

A method written as a numbered list is read in one direction, from the top down. That suits a method which is mostly a straight run of actions. It suits a robot far less well, because a robot’s method is full of forks: if this, do that; otherwise do something else; then go back and look again.

Prose handles forks badly. Follow the words “return to step 4” three or four times in a list of nine steps and you begin to lose track of which branch you are on. A picture handles them naturally, because the branches become lines you can put a finger on.

NoteDefinition

A flowchart is a diagram that represents the steps of an algorithm as standard symbols joined by arrows, with the arrows showing the order in which control passes from one step to the next.

Three things follow from drawing the method rather than listing it. Logical errors surface early, because a branch that leads nowhere is visible as a line ending in blank paper. The method becomes something two people can argue over without either of them knowing the same programming language. And when the code is finally written, each symbol on the chart corresponds to something the code must contain, so nothing quietly goes missing.

A flowchart is not a decoration added after the program works. It belongs to the stage before the program exists.

12.3.2 The Standard Symbols

The shapes are standardised so that a chart drawn in one workshop can be read in another. Five of them cover everything this chapter needs, and they are shown together in Fig. 12.3-1.

Diagram

Fig. 12.3-1 – the five standard flowchart symbols drawn in a single row, each with its name printed beneath it and a robotics label inside it: an oval containing “Start”; a rectangle containing “Set left motor speed to 160 rpm”; a diamond containing “Is distance greater than 20 cm?” with two exit arrows labelled Yes and No; a parallelogram containing “Read ultrasonic sensor”; and a plain arrow labelled “Flowline”

Table 12.3 – the standard flowchart symbols, the meaning each carries and a typical use in a robot’s method | Symbol | Name | What it represents | Typical use in a robot | |—|—|—|—| | Oval | Terminator | The single entry point or exit point of the method | Start at power-on; End at a controlled shutdown | | Rectangle | Process | One definite action carried out by the controller | Set a counter to zero; stop both motors | | Diamond | Decision | A question with clearly separated answers | Is the distance greater than 20 cm? | | Parallelogram | Input or output | Data entering or leaving the system | Read the infrared sensor; display the count | | Arrow | Flowline | The direction in which control passes | The return line from the last step to the first |

Two of these are worth separating carefully, because students routinely merge them. A rectangle holds an action the controller performs on its own; a parallelogram holds a movement of data across the boundary of the system, either in from a sensor or out to a display, a person or another device. Reading a sensor is therefore a parallelogram, while calculating a distance from that reading is a rectangle.

12.3.3 Rules for Drawing a Flowchart

The symbols alone do not make a chart readable. A small set of habits does.

Every chart has exactly one entry and one exit, both drawn as ovals. In a robot’s chart the exit is often reached only when power is removed, and it is still drawn, because a chart with no ending is a chart nobody can check.

Flow runs from top to bottom and from left to right. When control must return to an earlier point, the line is taken back cleanly down one side rather than threaded through the middle of the diagram.

Every rectangle holds one action. “Stop both motors and turn to the right” is two actions and belongs in two boxes, because the code will contain two instructions and the chart is what the code is checked against.

Every diamond holds a question that can be answered without judgement, and every exit from it is labelled. Two labels are usual — Yes and No — but a comparison may carry three, marked less than, equal to and greater than. No exit may be left blank, and no exit may be left dangling; each must arrive at a process, an input or output, another decision, or a point already on the chart.

A decision with a branch that leads nowhere What students do: draw a diamond asking whether an obstacle is close, take the Yes branch to a “Stop motors” box, and leave the No branch unlabelled or trailing off the page. What happens: the case that occurs most of the time — the path being clear — has no defined behaviour. Whoever writes the code invents something for it, and the robot’s ordinary behaviour ends up being the part nobody designed. The fix: label both exits before drawing anything after either of them, and follow each one until it reaches a box or rejoins the chart.

12.3.4 Flowcharts for Robotic Tasks

Consider a short belt conveyor in a school workshop. An infrared sensor watches a point on the belt, and the belt must stop as soon as five components have passed the sensor.

Written out, the method is short: start the belt, watch the sensor, count each component as it goes by, and stop when the count reaches five. Drawn out, as in Fig. 12.3-2, it shows something the sentence hides.

Diagram

Fig. 12.3-2 – flow: oval “Start” → rectangle “Configure sensor and belt motor pins” → rectangle “Set count to 0” → rectangle “Start the belt” → parallelogram “Read the infrared sensor” → diamond “Is a component in front of the sensor?” with the No branch returning to the “Read the infrared sensor” parallelogram and the Yes branch continuing → parallelogram “Read the infrared sensor” (second read, labelled “wait for the component to pass”) → diamond “Has the component cleared the sensor?” with the No branch returning to that second read and the Yes branch continuing → rectangle “Add 1 to count” → parallelogram “Display count” → diamond “Is count equal to 5?” with the No branch returning to the first “Read the infrared sensor” parallelogram and the Yes branch continuing → rectangle “Stop the belt” → oval “End”

The hidden difficulty is in the middle of the chart. A component does not flash past the sensor; it sits in front of it for a noticeable time. If the count were increased every time the sensor reported a blocked path, a single component resting under the beam would be counted again and again, at whatever rate the controller happened to be looking.

The chart deals with this by counting a component only after it has gone. The first decision waits for the component to arrive; the second waits for it to clear; only then does the counter advance. A counter is simply a stored number that a step increases by a fixed amount, and it is one of the few pieces of memory a small robot’s method usually needs.

Q. Why is the component counted when it leaves the sensor rather than when it arrives?

A. Because what the sensor reports is a state, not an event. It says “blocked” continuously for as long as the component is in the way. Counting on the state would count one component many times. Counting on the change from blocked to clear happens exactly once per component, however slowly the belt runs. Section 12.7 shows the hardware answer to the same problem, where the controller is told about the change instead of having to watch for it.

Trace the chart with a finger before writing any code for it. Enter at the oval, take the No branch at the first diamond a few times as a component approaches, take Yes, wait at the second diamond while it passes, advance the count, and arrive at the last decision. Do this four times over and you will be back at the top with the count at 4, which is exactly what should happen. A method that survives this kind of finger-tracing rarely surprises you once it is running.

12.4 Pseudocode

12.4.1 What Pseudocode Is

A flowchart shows the shape of a method beautifully and writes it out very slowly. Redrawing a chart because one threshold changed is tedious, and a chart with thirty boxes stops fitting on a page. There is a third form, closer to the code that will finally be written, and much quicker to edit.

NoteDefinition

Pseudocode is a description of a method written in structured, plain-language statements that follow the shape of a program, without obeying the syntax rules of any particular programming language.

Pseudocode is deliberately not a language. There is no compiler for it and no fixed grammar, so nothing can be wrong in it except the thinking. Keywords are conventionally written in capitals, actions in ordinary words, and blocks are marked by indentation and by a matching closing line. That is the whole apparatus.

Its usefulness comes from the fact that it can be read by a person and translated by anyone. A student writing embedded C for an Arduino UNO and a student writing Python for a Raspberry Pi can work from the same pseudocode and produce two programs that behave identically.

Table 12.4 – the three forms in which a method is written before it becomes a program | Feature | Method in words | Flowchart | Pseudocode | |—|—|—|—| | Form | numbered sentences | standard symbols joined by arrows | structured statements with keywords and indentation | | Best at | stating what is to be done and why | showing branches and loops at a glance | showing the exact structure the code will have | | Weakness | branches become hard to follow | slow to draw, and slow to change | looks like code, so errors in it look harmless | | Who reads it easily | anyone | anyone, in any language | someone who has seen a program before | | Distance from the finished code | greatest | middling | least |

None of the three replaces the other two. The words settle what the machine is for, the chart exposes the branching, and the pseudocode fixes the structure that the code will copy.

12.4.2 The Constructs Used in Pseudocode

Only a handful of constructs are needed, and all of them have direct counterparts in every programming language you will meet.

Straight sequence needs no keyword. Statements such as SET, READ, STOP and DISPLAY are simply written one below another and carried out in that order.

Selection chooses between paths.

IF <condition> THEN
    <steps carried out when the condition holds>
ELSE
    <steps carried out otherwise>
END IF

The closing line matters more than it looks. Without END IF there is nothing to say where the choice finishes and the ordinary flow resumes, and a reader has to guess whether the next statement belongs inside the block or after it. Guessing at this stage produces a program that is indented prettily and behaves wrongly.

Repetition, or iteration, comes in two shapes. When the number of repetitions is known in advance, a counted loop is used.

FOR <counter> FROM <first> TO <last>
    <steps repeated once for each value>
END FOR

When the number is not known and depends on a condition, a conditional loop is used instead.

WHILE <condition>
    <steps repeated for as long as the condition holds>
END WHILE

A robot’s main behaviour is usually written as REPEAT CONTINUOUSLY … END REPEAT, which is a conditional loop whose condition is simply that power is still applied. Comments are added after a double slash, exactly as they will be in the finished sketch.

12.4.3 From Pseudocode to Program

Take a task that every battery-powered robot needs and that no robot advertises: the guard that stops the machine before its battery is damaged.

A two-cell lithium polymer pack is nominally 7.4 V, and its cells should not be taken below roughly 3.3 V each, so the pack is not allowed to fall below about 6.6 V under load. The controller cannot read 7.4 V directly, because its analogue input must never be given more than 5 V; the pack is therefore read through a two-resistor divider that halves the voltage before it reaches pin A0. Section 12.5 deals with how that reading is converted into volts.

Low-battery guard for a two-cell lithium polymer pack Input: the pack voltage, read through a halving divider on analogue pin A0 and converted to volts. Output: motor drive permitted or removed, and a warning indicator. Method:

START
SET LOW_LIMIT to 6.6                 // volts, measured at the pack
SET shutdown to FALSE
CONFIGURE the motor channels and the warning LED
STOP both motors

:::


REPEAT CONTINUOUSLY
    READ battery_voltage from the divider on A0

    IF battery_voltage < LOW_LIMIT THEN
        SET shutdown to TRUE
    END IF

    IF shutdown IS TRUE THEN
        STOP both motors
        BLINK the warning LED
    ELSE
        CARRY OUT the robot's normal driving task
    END IF
END REPEAT
END

Why it works: the first decision only ever sets the shutdown state, and never clears it. Once the pack has been seen below the limit, drive stays removed for the rest of the session, however the reading behaves afterwards. This is deliberate: a loaded battery sags while the motors pull current and recovers the moment they stop, so a guard that simply compared the live reading each time would stop the robot, watch the voltage rise, start it again, and repeat that cycle several times a second. Watch out: a brief sag when a motor stalls can trip the guard early. The usual refinement is to require the low reading on several consecutive passes before setting the shutdown state. Note also that the divider must share its ground with the controller, or the reading has nothing to be measured against.

A lithium polymer pack taken below its minimum cell voltage may be permanently damaged and can become unsafe to charge afterwards. Discharge stops at the guard limit, not when the robot slows down. Packs are charged only on the balance charger, in the open, under supervision, and never left unattended or stored on charge.

Two features of that pseudocode are worth naming, because both reappear throughout the chapter. The variable shutdown is a flag — a stored true-or-false value that records that something has happened, so that a later part of the method can act on it. And the line asking for the normal driving task stands for a whole method of its own; pseudocode is allowed to name a job at the level that matters here and leave its detail to be written separately.

Now look at what the translation into code will actually require. Every line above corresponds to something concrete, and Table 12.5 sets out the correspondence for an Arduino UNO.

Table 12.5 – how each pseudocode construct becomes an instruction in an Arduino sketch | Pseudocode | What the sketch will contain | |—|—| | SET LOW_LIMIT to 6.6 | a constant declared once, above the functions | | SET shutdown to FALSE | a global variable of type bool, given its starting value | | CONFIGURE the motor channels and the warning LED | pinMode() calls inside setup() | | STOP both motors (before the loop) | digitalWrite() calls inside setup() | | REPEAT CONTINUOUSLY … END REPEAT | the body of loop(), which repeats on its own | | READ battery_voltage from … A0 | analogRead(A0) followed by the conversion arithmetic | | IF … THEN … ELSE … END IF | if (…) { … } else { … } |

The mapping is close enough that writing the sketch becomes clerical work rather than design work, which is exactly the intention. The design was finished before the editor was opened.

One caution goes with that closeness. Because pseudocode looks like a program, it is tempting to treat it as one and stop checking it. It is still only a description, and a description that says IF battery_voltage > LOW_LIMIT where it meant less than will translate faithfully into a robot that shuts down when its battery is full.

12.5 Programming a Microcontroller

12.5.1 Embedded C and Why C Was Extended

The method now exists on paper. Turning it into something a board will run means writing it in a language the board’s compiler understands, and for microcontrollers that language is almost always C, in the extended form called embedded C.

NoteDefinition

Embedded C is the C programming language together with the extensions and conventions needed to work directly with the hardware of a microcontroller — its pins, its timers, its interrupts and its memory registers.

Standard C was designed for computers with an operating system, plenty of memory and a screen. A microcontroller has none of those. Embedded C keeps C’s structure and adds what a controller actually needs.

The first addition is direct hardware access. A program has to be able to set a single pin high, read the state of a switch, or start a timer, and these are operations on specific memory locations inside the chip rather than on ordinary variables.

The second is economy. An Arduino UNO has 2 kilobytes of working memory, which is smaller than a page of plain text. Code that wastes memory does not merely run slowly; it stops running.

The third is portability. A program written in embedded C for one family of controllers usually moves to another with modest changes, because the language stays the same even when the hardware underneath does not.

What you write is not what the chip runs. The compiler translates your source code into machine instructions, and the upload step writes those instructions into the board’s program memory, where they stay until they are overwritten. Removing power does not erase them; switching the board on again starts the same program from the beginning.

12.5.2 High-Level Functions Built on Embedded C

Setting a pin high by writing to a register is exact and unreadable. A beginner writing it has to know the chip’s register names, the bit position of the pin inside that register, and how to change one bit without disturbing the other seven.

The Arduino environment supplies a set of ready-made functions instead — pinMode(), digitalWrite(), digitalRead(), analogRead(), delay() and the rest. These are not a different language. They are libraries, themselves written in C and C++, which do the register work for you when you call them.

Three things are gained. The instruction says what it means, so digitalWrite(9, HIGH) can be read by someone who has never seen a datasheet. The same instruction works across different boards even though their registers differ. And the time spent learning register layouts is spent on the robot’s behaviour instead.

One thing is lost, and it is worth knowing about rather than discovering later. A ready-made function does more work than a direct register write: it checks which port the pin belongs to, whether a timer is using it, and so on. A digitalWrite() call therefore takes a few microseconds, while a direct register write takes a small fraction of one. For almost everything in this chapter that difference is irrelevant. In a program that must toggle a pin hundreds of thousands of times a second, it is decisive.

12.5.3 The Structure of a Sketch

A program written for an Arduino board is called a sketch, and it always has the same skeleton. Fig. 12.5-1 shows the order in which its parts are dealt with.

Diagram

Fig. 12.5-1 – flow showing the structure and execution order of a sketch: a vertical column of five labelled blocks in file order — “Library inclusion (#include)”, “Global variables and constants”, “setup()”, “loop()”, “User-defined functions” — with an arrow entering setup() marked “runs once at power-on or reset”, an arrow from setup() to loop() marked “control handed over”, a return arrow looping around loop() marked “repeats for as long as power is applied”, and dashed arrows from loop() out to the user-defined functions block and back marked “called when needed”

Library inclusion comes first. A library is prewritten code that adds functions the core language does not have, and it is brought in with #include. A servo motor needs #include <Servo.h>, a device on the I2C bus needs #include <Wire.h>, and a sketch that uses neither needs no include line at all.

Global variables and constants come next, outside every function, so that any part of the sketch can use them. Pin numbers and thresholds belong here, where a single edit changes them everywhere.

setup() runs once, at power-on or after a reset. Everything that must be true before the robot starts working goes inside it: the direction of every pin, the starting state of every output, the opening of the serial link, the attaching of a servo.

loop() runs immediately afterwards and then repeats, without being asked, for as long as power lasts. It holds the sensing, the deciding and the acting. This is the REPEAT CONTINUOUSLY of your pseudocode, supplied by the board rather than written by you.

User-defined functions are placed after loop() and are described in Section 12.5.7.

// 1. Library inclusion, only if a library is needed
#include <Servo.h>

// 2. Global constants and variables
const int ledPin = 13;

void setup() {
    // 3. Work done once
    pinMode(ledPin, OUTPUT);
}

void loop() {
    // 4. Work repeated for as long as power is applied
}

12.5.4 Variables, Constants and Data Types

A variable is a named place in memory holding a value that may change while the program runs. A constant is a named value that may not, and it is written with the keyword const in front of its declaration. Using const for a pin number is not fussiness: it means the compiler will refuse an accidental attempt to change it, and the error appears on your screen rather than on the workbench.

Every name must be given a type, because the type decides how much memory the value occupies and what range it may cover. Table 12.6 lists the types this chapter uses, with the sizes they have on an Arduino UNO.

Table 12.6 – data types used in this chapter, with their size and range on an Arduino UNO and their usual purpose | Type | Size | Range | Usual purpose | |—|—|—|—| | bool | 1 byte | true or false | a flag recording that something has happened | | byte | 1 byte | 0 to 255 | a pin number, a small count, a duty-cycle value | | int | 2 bytes | −32,768 to 32,767 | the result of analogRead(), general counting | | long | 4 bytes | −2,147,483,648 to 2,147,483,647 | pulse durations in microseconds | | unsigned long | 4 bytes | 0 to 4,294,967,295 | timestamps taken from millis() | | float | 4 bytes | about ±3.4 × 10³⁸, with 6 to 7 significant digits | converted voltages, distances, speeds |

Sizes belong to the board, not to the language. On a 32-bit board such as the ESP32, an int occupies 4 bytes and reaches into the billions. Code that quietly relies on an int being 2 bytes is code that behaves differently when it is moved.

Storing a millisecond timestamp in an int What students do: write int now = millis(); because int is the type they use for everything else. What happens: millis() returns an unsigned long, and an int on an Arduino UNO cannot hold a value above 32,767. The sketch compiles without complaint, works for the first 32.7 seconds, and then the stored time jumps to a large negative number. Every interval calculated from it becomes nonsense, and the fault appears only after the robot has been running for a while — which is the hardest kind of fault to catch. The fix: unsigned long now = millis();. Any variable that holds or is compared with a time from millis() or micros() must be unsigned long.

12.5.5 Digital Input and Output

A digital pin has two states, and three instructions cover everything you can do with one. pinMode() decides what the pin is for, digitalWrite() sets an output, and digitalRead() reports an input.

configuring a pin with pinMode()

pinMode(pin_number, mode);

Parts: pin_number — the number of a digital pin, or an analogue pin name such as A0; mode — one of INPUT, OUTPUT or INPUT_PULLUP. Returns / Effect: nothing is returned. The pin is set to drive or to sense, and with INPUT_PULLUP the chip’s internal pull-up resistor is connected between the pin and the supply. Example:

pinMode(7, OUTPUT);
pinMode(2, INPUT_PULLUP);

INPUT_PULLUP deserves a moment. A switch has two terminals and can only connect a pin to something; it cannot supply a voltage. If one terminal goes to the pin and the other to ground, then a pressed switch pulls the pin to 0 V, but a released switch leaves the pin connected to nothing at all. An input connected to nothing does not read a steady value — it drifts with whatever electrical noise is nearby. INPUT_PULLUP fixes this by tying the pin gently to 5 V through an internal resistor of roughly 20 kΩ to 50 kΩ, so a released switch reads HIGH and a pressed switch reads LOW.

That inversion — pressed means LOW — surprises everyone once. It is called an active-low arrangement, and Section 12.6.6 shows how it is handled in the logic.

Now a real task. A robot arm’s belt drive must stop the instant a lever limit switch at the end of its travel is struck, and light a warning indicator while the switch is held. Fig. 12.5-2 shows the circuit.

Diagram

Fig. 12.5-2 – circuit: an Arduino UNO with a lever limit switch, an L298N motor driver module, a DC gear motor and an indicator LED. One terminal of the limit switch to Arduino pin 2, the other terminal to Arduino GND. Arduino pin 9 to the L298N ENA input, pin 7 to IN1, pin 8 to IN2. L298N OUT1 and OUT2 to the two motor terminals. L298N +12V terminal to the positive terminal of a 7.4 V pack, L298N GND terminal to the pack negative terminal AND to Arduino GND, with this shared connection drawn as a single heavy ground rail and labelled “common ground — not optional”. Arduino pin 6 through a 220 Ω resistor to the LED anode, LED cathode to the ground rail. Every wire labelled with its pin name.

Stopping a belt drive when a limit switch is struck Problem: while the lever limit switch on pin 2 is free, the motor runs forward; while it is pressed, drive is removed and the indicator lights. Board and components: Arduino UNO; L298N motor driver module; one DC gear motor; one lever limit switch; one LED with a 220 Ω series resistor; a 7.4 V pack for the motor supply. The board is powered from USB during bench testing. Wiring: limit switch between pin 2 and GND. Pin 9 to ENA, pin 7 to IN1, pin 8 to IN2 on the L298N. OUT1 and OUT2 to the motor. The pack’s positive terminal to the module’s +12V terminal, the pack’s negative terminal to the module’s GND terminal and to the Arduino’s GND. LED anode through 220 Ω to pin 6, cathode to ground.

const int limitPin = 2;      // lever switch, other terminal to ground
const int enablePin = 9;     // ENA on the L298N
const int in1Pin = 7;        // IN1 on the L298N
const int in2Pin = 8;        // IN2 on the L298N
const int warnPin = 6;       // indicator LED through 220 ohm

void setup() {
    pinMode(limitPin, INPUT_PULLUP);
    pinMode(enablePin, OUTPUT);
    pinMode(in1Pin, OUTPUT);
    pinMode(in2Pin, OUTPUT);
    pinMode(warnPin, OUTPUT);

    // Direction is fixed once; only the enable line is switched afterwards.
    digitalWrite(in1Pin, HIGH);
    digitalWrite(in2Pin, LOW);

    // Start in the safe state.
    digitalWrite(enablePin, LOW);
    digitalWrite(warnPin, LOW);
}

void loop() {
    int limitState = digitalRead(limitPin);

    if (limitState == LOW) {          // contact made: the lever is pressed
        digitalWrite(enablePin, LOW);
        digitalWrite(warnPin, HIGH);
    } else {
        digitalWrite(enablePin, HIGH);
        digitalWrite(warnPin, LOW);
    }
}

Sample output / observed behaviour: nothing is printed. With the lever free the motor turns steadily and the indicator is dark. Pressing the lever removes drive and lights the indicator within one pass of loop(), which here is a small fraction of a millisecond. Releasing the lever restarts the motor immediately, because nothing in this sketch remembers that the switch was ever struck. Identifier description:

[Table:] Table 12.7 – identifiers used in the limit-switch program | Identifier | Type | Purpose | |—|—|—| | limitPin | const int | pin number of the lever limit switch | | enablePin | const int | pin number driving the ENA input of the L298N | | in1Pin | const int | pin number driving IN1, one half of the direction pair | | in2Pin | const int | pin number driving IN2, the other half of the direction pair | | warnPin | const int | pin number driving the indicator LED | | limitState | int | the state read from the limit switch on this pass | [/Table]

Notes: the motor’s current never passes through an Arduino pin. A pin can supply about 20 mA safely, while even a small gear motor draws hundreds of milliamperes and far more at stall, so the pin drives only the L298N’s logic input and the module passes the pack’s current to the motor. Direction is set once in setup() because it never changes here; switching the single enable line is quicker to read and quicker to check than rewriting both direction pins. Compare the behaviour with the guard of Section 12.4: that one latched deliberately, this one does not, and which is correct depends entirely on whether releasing the switch means the danger has passed.

Leaving out the common ground What students do: connect the signal wires from the board to the driver module, and power the module from a separate battery pack, without joining the pack’s negative terminal to the board’s GND. What happens: the two circuits have no shared reference, so “5 V” from the board means nothing to the module. The motor may twitch, run continuously, ignore the enable line, or behave differently each time the wiring is touched. The code is not at fault and no amount of reading it will help. The fix: whenever two powered circuits exchange a signal, their grounds are joined. Signal and ground travel together, always.

12.5.6 Reading an Analogue Sensor

Many sensors do not answer yes or no. They produce a voltage somewhere between 0 V and the supply, and the value of that voltage is the measurement. To use it, the controller must convert the voltage into a number, which is the job of the analogue-to-digital converter built into the chip.

reading an analogue input with analogRead()

analogRead(analog_pin);

Parts: analog_pin — one of the analogue inputs, A0 to A5 on an Arduino UNO. Returns / Effect: an int between 0 and 1023, in proportion to the input voltage measured against the reference, which is 5 V by default on an UNO. A single conversion takes roughly 100 µs. Example:

int raw = analogRead(A0);

The converter on an UNO is a 10-bit device, which is why the numbers run from 0 to 1023: ten bits give \(2^{10} = 1024\) distinct values. To turn a reading back into volts, multiply by the size of one step:

\[V = n \times \frac{5.0}{1023.0}\]

where \(n\) is the value returned and \(V\) is in volts. You will also see this written with 1024 in the denominator, and both are defensible — 1024 is the true number of steps, while 1023 makes the largest reading correspond exactly to 5.00 V. The difference between them is about one part in a thousand, far smaller than the tolerance of the 5 V supply that is being used as the reference, so neither choice will be the thing that spoils a measurement.

An analogue pin never sees more than 5 V. The two-cell lithium polymer pack of Section 12.4 sits between 6.6 V and 8.4 V, so it is read through two equal resistors in series across the pack, with the mid-point taken to A0. That halves the voltage, and multiplying the converted result by two recovers the pack voltage. Fig. 12.5-3 shows the arrangement, and the program below is the pseudocode of Section 12.4.3 written out.

Diagram

Fig. 12.5-3 – circuit: a 7.4 V two-cell lithium polymer pack; from its positive terminal a 10 kΩ resistor to a node, and from that node a second 10 kΩ resistor to the pack negative terminal; the node connected to Arduino UNO pin A0 and labelled “half the pack voltage”. Pack negative terminal joined to Arduino GND and to the L298N GND terminal, drawn as one ground rail labelled “common ground”. Arduino pin 9 to the L298N ENA input; Arduino pin 6 through a 220 Ω resistor to an LED, LED cathode to the ground rail. Both resistor values and the pin names labelled.

Latching low-battery guard for a two-cell pack Problem: read the pack voltage twice a second; if it is ever below 6.6 V, remove drive and light the warning indicator, and keep it removed for the rest of the session. Board and components: Arduino UNO; two 10 kΩ resistors forming the divider; a 7.4 V two-cell lithium polymer pack; L298N motor driver module; one LED with a 220 Ω series resistor. Wiring: pack positive to the first 10 kΩ resistor, the junction of the two resistors to A0, the second 10 kΩ resistor to pack negative. Pack negative joined to Arduino GND and to the L298N GND terminal. Pin 9 to ENA. LED anode through 220 Ω to pin 6, cathode to ground.

const int sensePin = A0;      // mid-point of the divider
const int enablePin = 9;      // ENA on the L298N
const int warnPin = 6;        // warning LED through 220 ohm

const float DIVIDER_FACTOR = 2.0;   // two equal resistors halve the pack voltage
const float LOW_LIMIT = 6.6;        // volts, measured at the pack

bool shutdownLatched = false;

void setup() {
    pinMode(enablePin, OUTPUT);
    pinMode(warnPin, OUTPUT);
    digitalWrite(enablePin, LOW);
    digitalWrite(warnPin, LOW);
    Serial.begin(9600);
}

void loop() {
    int raw = analogRead(sensePin);
    float packVolts = raw * (5.0 / 1023.0) * DIVIDER_FACTOR;

    if (packVolts < LOW_LIMIT) {
        shutdownLatched = true;
    }

    if (shutdownLatched) {
        digitalWrite(enablePin, LOW);
        digitalWrite(warnPin, HIGH);
    } else {
        digitalWrite(enablePin, HIGH);
        digitalWrite(warnPin, LOW);
    }

    Serial.print("Battery: ");
    Serial.print(packVolts, 2);
    Serial.println(" V");

    delay(500);
}

Sample output / observed behaviour: with the raw readings 759, 700, 670 and 720 arriving on four successive passes, the serial monitor shows

Battery: 7.42 V
Battery: 6.84 V
Battery: 6.55 V
Battery: 7.04 V

and the enable line goes low on the third of those passes and stays low, with the warning LED lit from that point onwards. Identifier description:

[Table:] Table 12.8 – identifiers used in the low-battery guard | Identifier | Type | Purpose | |—|—|—| | sensePin | const int | analogue pin reading the divider mid-point | | enablePin | const int | pin driving the ENA input of the L298N | | warnPin | const int | pin driving the warning LED | | DIVIDER_FACTOR | const float | multiplier that converts divider voltage back to pack voltage | | LOW_LIMIT | const float | pack voltage below which drive is removed | | shutdownLatched | bool | flag recording that the pack has been seen below the limit | | raw | int | the value returned by analogRead() on this pass | | packVolts | float | the pack voltage in volts, after conversion | [/Table]

Notes: the conversion is written as 5.0 / 1023.0 rather than 5 / 1023 on purpose. Written with whole numbers, C would divide 5 by 1023 as integers and arrive at 0, and every reading would come out as 0.00 V. The delay(500) is acceptable here only because this sketch does nothing else; Section 12.8 replaces it once the guard has to share the controller with the robot’s real work.

the low-battery guard on four successive readings from analogRead()

Pass raw packVolts shutdownLatched after the test enablePin Line printed
1 759 7.42 false HIGH Battery: 7.42 V
2 700 6.84 false HIGH Battery: 6.84 V
3 670 6.55 true LOW Battery: 6.55 V
4 720 7.04 true LOW Battery: 7.04 V

Result: drive is removed on pass 3 and is still removed on pass 4, even though the reading has recovered to 7.04 V. The trace shows the latch doing exactly the job it was designed for — a pack sags under load and springs back when the motors stop, and without the flag the robot would restart itself and repeat the cycle indefinitely.

With nothing at all connected to A0, upload a short sketch that prints analogRead(A0) twice a second, and watch the serial monitor for a minute with your hand moving near the board. Write down first what you expect a disconnected input to read, then what it actually does.

12.5.7 User-Defined Functions

The two programs above are short enough to read in one sitting. Real sketches are not, and the cure is to give a name to each job the sketch does and write that job once.

defining a function

return_type function_name(parameter_list) {
    statements;
    return value;
}

Parts: return_type — the type of the value handed back, or void if nothing is handed back; function_name — a descriptive name in camelCase; parameter_list — zero or more typed values the function is given, separated by commas; return — the statement that hands the value back, omitted when the return type is void. Returns / Effect: the named work is carried out wherever the function is called, and the value, if any, replaces the call. Example:

float packVoltage(int pin) {
    int raw = analogRead(pin);
    return raw * (5.0 / 1023.0) * 2.0;
}

With that function present, the first two lines of loop() in the guard become a single readable line:

float packVolts = packVoltage(sensePin);

Three benefits follow, and only the first is obvious. The sketch reads as a list of jobs rather than a list of instructions. The conversion arithmetic exists in exactly one place, so correcting the divider factor is one edit rather than a search. And the function can be tested on its own, by calling it and printing the result, before it is trusted inside a larger program — a habit Section 12.9 relies on completely.

A function is defined once and may be called as often as you like. Calling it does not copy it; control jumps to the function, runs it, and returns to the instruction after the call.

12.5.8 Programming a Single-Board Computer

A single-board computer such as a Raspberry Pi runs a full operating system, so programming it feels closer to programming a laptop. Python is the usual language, the program is a file that is run rather than a sketch that is uploaded, and the perpetual behaviour that loop() provides on a microcontroller has to be written out as a while True: loop.

The same limit-switch job of Section 12.5.5 is shown below on a Raspberry Pi, purely so that the two platforms can be compared line for line. The switch goes between GPIO17 and ground, and GPIO27 drives the enable input of the same motor driver module.

from gpiozero import LED, Button
from time import sleep

limit_switch = Button(17)     # pulled up internally; pressed connects to ground
drive_enable = LED(27)        # drives the ENA input of the motor driver

while True:
    if limit_switch.is_pressed:
        drive_enable.off()
    else:
        drive_enable.on()
    sleep(0.02)

Two differences matter more than the change of language.

The first is electrical. A Raspberry Pi’s pins work at 3.3 V and are not tolerant of 5 V; connecting a 5 V sensor output directly to one can destroy the pin. The board also has no analogue-to-digital converter at all, so the divider circuit of Fig. 12.5-3 cannot simply be moved across — an external converter chip would be needed to read the pack voltage.

The second is about timing. The operating system decides when your program gets the processor, so a Raspberry Pi cannot promise to respond to an event within a fixed number of microseconds. A microcontroller, running nothing but your program, can.

Table 12.9 – microcontroller and single-board computer as the controller of a robot | Feature | Microcontroller (Arduino UNO) | Single-board computer (Raspberry Pi) | |—|—|—| | What it runs | one program, directly on the hardware | an operating system, with your program as one task among many | | Usual language | embedded C, through the Arduino functions | Python, among others | | Start-up | running within a fraction of a second of power | boots for tens of seconds before the program starts | | Timing behaviour | predictable to the microsecond | depends on what else the operating system is doing | | Analogue input | built in, 10-bit on an UNO | none on the board; an external converter is required | | Pin logic level | 5 V on an UNO | 3.3 V, and not 5 V tolerant | | Best suited to | motor control, timing, reading sensors, reacting fast | vision, networking, data storage, heavy computation |

The choice between them is not a ranking, and large robots frequently use both: a single-board computer to decide where to go, and a microcontroller wired to the motors and sensors to make sure it gets there on time.

12.6 Decision-Making with Boolean Operators

12.6.1 Conditions and the if–else Statement

Everything a robot does that could be called a decision comes down to one thing: a question whose answer is either true or false. Is the distance greater than 20 cm? Is the guard door closed? Is the battery below its limit? Such a question is called a condition, and its answer is a Boolean value — one of exactly two states, true or false, named after the mathematician George Boole.

Conditions are built with comparison operators. == asks whether two values are equal, != whether they differ, and <, >, <= and >= compare magnitudes. Each of them produces a Boolean, nothing else.

choosing between two paths with if–else

if (condition) {
    statements carried out when the condition is true;
} else {
    statements carried out when the condition is false;
}

Parts: condition — any expression that evaluates to true or false; the braces enclose the block belonging to each branch; the whole else part may be omitted when nothing is to be done in the false case. Returns / Effect: exactly one of the two blocks is carried out on each pass, never both. Example:

if (distanceCm > 20) {
    digitalWrite(enablePin, HIGH);
} else {
    digitalWrite(enablePin, LOW);
}

When more than two outcomes are needed, conditions are chained with else if, and the first one that is true wins. The chain is tested in the order written, so the order is part of the design, not a matter of taste.

Writing = where == was meant What students do: type if (state = HIGH) instead of if (state == HIGH). What happens: = assigns rather than compares. The variable state is overwritten with HIGH, the whole expression takes that value, and because HIGH is 1 the condition is true every single time. The sketch compiles, and the robot behaves as though the sensor were permanently triggered. The fix: one equals sign stores, two equals signs ask. Read every condition aloud as a question before uploading; “state gets HIGH” is not a question.

12.6.2 The AND Operator

A single condition is rarely enough. A press must come down only when a part is in position and the guard door is closed. Both facts must hold at once, and the operator that demands this is AND, written &&.

Table 12.10 – truth table of the AND operator | A | B | A && B | |—|—|—| | false | false | false | | false | true | false | | true | false | false | | true | true | true |

The pattern is easy to remember from the one line that differs: AND is true only when everything it joins is true. A single false condition anywhere in a chain of && makes the whole expression false.

This is why AND is the operator of permission. Conditions that must all be satisfied before something dangerous or expensive happens are joined with &&, and each one added makes the machine harder to start, which is exactly the intention.

Assuming partPresent and guardClosed have already been read as Booleans:

if (partPresent && guardClosed) {
    digitalWrite(pressPin, HIGH);   // the press may descend
} else {
    digitalWrite(pressPin, LOW);
}

Q. If the first condition in a chain of && is already false, does the controller still test the rest?

A. No. C evaluates && from left to right and stops the moment the answer is settled, because nothing after a false condition can rescue the expression. This is called short-circuit evaluation, and it is useful as well as quick: the cheapest or most decisive test is put first, so a long calculation on the right is skipped whenever the guard on the left already says no.

Two operators look alike and are not. && is the logical AND used with conditions; & is the bitwise AND, which combines the individual bits of two numbers. Conditions take &&.

12.6.3 The OR Operator

The opposite need arises just as often. A mobile robot carries a bumper switch on each front corner, and striking either one must stop it. Here any single condition is sufficient, and the operator is OR, written ||.

Table 12.11 – truth table of the OR operator | A | B | A \|\| B | |—|—|—| | false | false | false | | false | true | true | | true | false | true | | true | true | true |

Again one line does the remembering: OR is false only when everything it joins is false. Adding another condition to a chain of || makes the outcome easier to trigger, not harder.

OR is therefore the operator of alarm. Several independent things can each mean trouble, and any of them must be enough to act on.

if (leftBumper || rightBumper) {
    digitalWrite(enablePin, LOW);   // either bumper removes drive
}

Notice the division of labour that is emerging. Reasons to stop are gathered with ||; permissions to run are gathered with &&. Almost every safety rule you will meet in this subject is built from those two habits.

12.6.4 The NOT Operator

The third operator takes a single condition and reverses it. NOT is written ! and is placed in front of what it reverses.

Table 12.12 – truth table of the NOT operator | A | !A | |—|—| | false | true | | true | false |

NOT exists because a great many sensors report the opposite of what the decision needs. An infrared object sensor reports that an object is present; the conveyor beneath it must run when no object is present. Rather than twisting the sensor’s meaning, the reading is stored honestly and reversed at the point of use.

bool objectPresent = (digitalRead(sensorPin) == HIGH);

if (!objectPresent) {
    digitalWrite(enablePin, HIGH);   // belt runs only when nothing is under the sensor
} else {
    digitalWrite(enablePin, LOW);
}

Read that condition aloud and it says what the specification said: run when there is not an object present. A program whose conditions can be read aloud as English sentences is a program whose faults are visible.

12.6.5 Combining the Operators in One Control Rule

Real rules use all three at once. Consider a sorting station in a school workshop: a short belt that carries components past a diverter, with a hinged guard over the moving parts.

The station is allowed to run only when it has been armed by the operator, and the guard is closed, and the emergency stop is healthy, and neither of the two bumper switches at the ends of the belt has been struck. Written as one sentence that is exactly what it says, and written as one expression it needs &&, || and ! together.

One wiring decision in Fig. 12.6-1 deserves attention before the code. The emergency stop is a normally-closed contact: the circuit through it is complete while everything is well, and pressing the button breaks it. With the internal pull-up enabled, an intact loop reads LOW and a pressed button reads HIGH — and so does a wire that has come loose or been cut. The system is therefore made to treat any break as a stop, which is what fail-safe wiring means.

Diagram

Fig. 12.6-1 – circuit: an Arduino UNO with five switch inputs and a motor driver. Arm toggle switch between pin 2 and GND; guard door switch between pin 3 and GND; emergency-stop button with a normally-closed contact between pin 4 and GND, labelled “normally closed — a break reads HIGH”; left bumper switch between pin 5 and GND; right bumper switch between pin 6 and GND; all five inputs labelled “INPUT_PULLUP”. Pin 9 to the L298N ENA input, pin 8 to IN1, pin 10 to IN2, OUT1 and OUT2 to the belt motor. L298N +12V terminal to a 7.4 V pack positive, L298N GND terminal to pack negative and to Arduino GND, drawn as one heavy ground rail labelled “common ground”. Pin 7 through a 220 Ω resistor to the halt-indicator LED, cathode to the ground rail.

Interlock rule for a workshop sorting station Problem: run the belt only while the station is armed, the guard is closed, the emergency-stop loop is intact and neither bumper has been struck; light the halt indicator at all other times. Board and components: Arduino UNO; L298N motor driver module; one DC gear motor driving the belt; an arm toggle switch; a guard door switch; an emergency-stop button with a normally-closed contact; two bumper switches; one LED with a 220 Ω series resistor; a 7.4 V pack for the motor supply. Wiring: each of the five switches connects its pin to GND, with the internal pull-up doing the rest — arm on pin 2, guard on pin 3, emergency stop on pin 4, left bumper on pin 5, right bumper on pin 6. Pin 9 to ENA, pin 8 to IN1, pin 10 to IN2, OUT1 and OUT2 to the motor. Pack positive to +12V, pack negative to the module’s GND and to the Arduino’s GND. LED anode through 220 Ω to pin 7, cathode to ground.

const int armPin        = 2;
const int guardPin      = 3;
const int stopPin       = 4;    // normally-closed emergency-stop loop
const int bumpLeftPin   = 5;
const int bumpRightPin  = 6;

const int haltLedPin    = 7;
const int in1Pin        = 8;
const int enablePin     = 9;    // ENA on the L298N
const int in2Pin        = 10;

void setup() {
    pinMode(armPin, INPUT_PULLUP);
    pinMode(guardPin, INPUT_PULLUP);
    pinMode(stopPin, INPUT_PULLUP);
    pinMode(bumpLeftPin, INPUT_PULLUP);
    pinMode(bumpRightPin, INPUT_PULLUP);

    pinMode(haltLedPin, OUTPUT);
    pinMode(in1Pin, OUTPUT);
    pinMode(in2Pin, OUTPUT);
    pinMode(enablePin, OUTPUT);

    // Belt direction is fixed; only the enable line is switched.
    digitalWrite(in1Pin, HIGH);
    digitalWrite(in2Pin, LOW);

    // Start halted.
    digitalWrite(enablePin, LOW);
    digitalWrite(haltLedPin, HIGH);
}

void loop() {
    // Electrical levels are turned into plain statements about the station.
    bool armed       = (digitalRead(armPin) == LOW);
    bool guardClosed = (digitalRead(guardPin) == LOW);
    bool stopHealthy = (digitalRead(stopPin) == LOW);      // loop intact
    bool leftHit     = (digitalRead(bumpLeftPin) == LOW);
    bool rightHit    = (digitalRead(bumpRightPin) == LOW);

    bool bumped = leftHit || rightHit;                     // either is enough
    bool mayRun = armed && guardClosed && stopHealthy && !bumped;

    if (mayRun) {
        digitalWrite(enablePin, HIGH);
        digitalWrite(haltLedPin, LOW);
    } else {
        digitalWrite(enablePin, LOW);
        digitalWrite(haltLedPin, HIGH);
    }
}

Sample output / observed behaviour: nothing is printed. The belt turns only when the arm switch is on, the guard is shut, the emergency stop is out and both bumpers are free; the halt indicator is lit in every other case, including when the emergency-stop wire is simply unplugged from pin 4. Identifier description:

[Table:] Table 12.13 – identifiers used in the interlock program | Identifier | Type | Purpose | |—|—|—| | armPin, guardPin, stopPin | const int | pin numbers of the arm switch, guard switch and emergency-stop loop | | bumpLeftPin, bumpRightPin | const int | pin numbers of the two bumper switches | | haltLedPin | const int | pin driving the halt indicator | | in1Pin, in2Pin | const int | pins setting the belt motor’s direction on the L298N | | enablePin | const int | pin driving the ENA input, which grants or removes drive | | armed, guardClosed, stopHealthy | bool | the three permissions, read this pass | | leftHit, rightHit | bool | whether each bumper is currently struck | | bumped | bool | true when either bumper is struck | | mayRun | bool | the single Boolean the actuation depends on | [/Table]

Notes: the sketch takes two deliberate steps between reading a pin and driving a motor. First every electrical level is converted into a named fact about the station, so that stopHealthy says what it means and nobody has to remember that LOW is the good state. Then the facts are combined into a single Boolean, mayRun, and only that Boolean is acted upon. Written this way, the rule in the code and the rule in the specification can be compared word for word, which is not true of a condition assembled directly from five digitalRead() calls.

the interlock rule on five states of the station

Case armed guardClosed stopHealthy leftHit rightHit bumped mayRun enablePin Halt LED
Normal running true true true false false false true HIGH off
Not armed false true true false false false false LOW on
Guard lifted true false true false false false false LOW on
Left bumper struck true true true true false true false LOW on
Stop pressed or wire broken true true false false false false false LOW on

Result: only the first case grants drive. The trace also shows what the operators are each doing — || lets one bumper speak for both, ! turns “a bumper was struck” into “no bumper was struck”, and && insists that every remaining permission be present at the same moment.

The guard switch exists to stop the belt when the guard is lifted, so taping or wedging it closed removes the only thing standing between a hand and a moving belt. Fingers, hair and sleeves stay clear of the belt and the diverter whenever the station is armed, and power is disconnected at the pack before any wire is moved.

Q5. A machine must act only when the system is switched on and an obstacle is close — or when either of two emergency switches is pressed. How are several conditions like these folded into one decision?

A. By joining them with the three Boolean operators. Conditions that must all hold at once are joined with AND, written &&; conditions of which any one is sufficient are joined with OR, written ||; and a condition whose sense has to be reversed — a sensor that reports what you want to be absent — is turned round with NOT, written !. Each condition is first read into a Boolean variable named after what it means, and the variables are then combined into a single Boolean that the actuator obeys. The expression that results, such as armed && guardClosed && stopHealthy && !bumped, reads as the same sentence the specification was written in.

12.6.6 Active-Low Signals and Logical Inversion

The interlock program inverted almost everything it read, and that was not a quirk of this one circuit. It is the normal state of affairs, and the reason is electrical rather than logical.

A mechanical switch cannot supply a voltage; it can only make or break a connection. The cheapest reliable arrangement therefore connects the switch between the pin and ground and lets the controller’s internal pull-up hold the pin high the rest of the time. Closing the switch pulls the pin down. A signal that means “yes” when it is at 0 V is called an active-low signal, and most switch inputs, many sensor modules and a good number of driver chips use one.

The mistake is to let the electrical convention leak into the logic, and write conditions such as if (digitalRead(guardPin) == LOW && digitalRead(stopPin) == LOW). That line is correct and unreadable, and six months later nobody can say which state was the safe one.

The habit worth forming has two steps and appears in every program in this section. Convert each reading once, into a Boolean named for the fact it represents. Then write the rule using those names, applying ! wherever the rule needs the opposite of what was named. Section 12.9 shows why this pays: when a station refuses to start, a program written this way can be made to report which of its named facts is false, and the fault is found in seconds rather than by guesswork at the wiring.

12.7 Interrupts

12.7.1 Why Polling Misses Events

Every program so far has found out about the world in the same way: loop() comes round, reads a pin, and acts on what it finds. Reading an input at intervals to see whether anything has changed is called polling, and for slow things it is perfectly good. A guard door stays open for seconds; a battery falls over minutes; a lever switch held by a mechanism stays held.

Polling fails when the thing to be noticed is brief. The controller only knows what was true at the instants it happened to look, and anything that begins and ends between two looks leaves no trace at all.

Consider a wheel fitted with a slotted disc and an optical sensor, the standard way of measuring how far a robot has travelled. Each slot passing the sensor produces one pulse, and the pulses are what must be counted.

How long is one encoder pulse, and can polling catch it?

Given: an encoder disc with 20 slots; the shaft turning at 300 rpm; slots and gaps of equal width; one pass through loop() taking 20 ms.

Find: the pulse rate, the duration of one pulse, and whether polling at that rate counts every pulse.

Method: pulses per second follow from the rotation rate and the number of slots. The period is the reciprocal of that rate, and equal slots and gaps make the high time half a period.

\[\text{rotation rate} = \frac{300}{60} = 5 \text{ rev/s}\]

\[\text{pulse rate} = 5 \text{ rev/s} \times 20 \text{ pulses/rev} = 100 \text{ Hz}\]

\[T = \frac{1}{100 \text{ Hz}} = 0.01 \text{ s} = 10 \text{ ms}, \qquad t_{\text{high}} \approx \frac{T}{2} = 5 \text{ ms}\]

pulses arrive at 100 Hz, each lasting about 5 ms, while the program looks only once every 20 ms. Two whole pulses pass between consecutive looks, so at best half of them are counted and the distance measurement is meaningless.

Check: 100 Hz from a small geared wheel is an ordinary figure, not an extreme one — at 1200 rpm the same disc gives 400 Hz and a high time of about 1.25 ms. The gap between what the hardware produces and what a polled loop can see gets worse, not better, as the robot speeds up.

The obvious fix — poll faster — works only until the loop has something else to do. One delay(), one long serial print, one distance measurement that waits for an echo, and the pass stretches again.

Fig. 12.7-1 shows the difficulty and its remedy on a common time axis.

Diagram

Fig. 12.7-1 – two timing sketches drawn one above the other on a shared horizontal time axis marked in milliseconds. Upper sketch, labelled “Polling”: a row of short input pulses at 10 ms spacing, and beneath it tick marks at 20 ms spacing labelled “the moment loop() reads the pin”, with arrows showing which pulses are seen and the missed pulses shaded and labelled “never noticed”. Lower sketch, labelled “Interrupt”: the same row of input pulses, with a vertical arrow from every pulse edge down to a small block labelled “ISR runs” inserted into a continuous bar labelled “main program”, and the main program bar resuming immediately after each block.

Table 12.14 – polling and interrupts as ways of noticing an event | Feature | Polling | Interrupt | |—|—|—| | How the event is noticed | the program looks at the pin | the hardware calls the program | | Worst-case delay in noticing | one full pass of loop() | a few microseconds | | Brief events | missed if shorter than a pass | caught, however brief | | Cost when nothing happens | the pin is read every pass anyway | nothing at all | | Program structure | simple, all in one place | split between loop() and a separate routine | | Suits | slow states — doors, batteries, levers held by a mechanism | edges — encoder ticks, emergency stops, echo returns |

12.7.2 What an Interrupt Is

The alternative is to stop watching and be told instead. The hardware of the microcontroller can be set to watch a pin on its own and, the moment the pin changes in a specified way, to force the processor to leave whatever it was doing and run a short routine written for that event.

NoteDefinition

An interrupt is a hardware signal that suspends the normal execution of a program so that a designated routine may be carried out at once, after which execution resumes at exactly the instruction that was suspended.

NoteDefinition

An interrupt service routine (ISR) is the function that is executed in response to a particular interrupt.

Fig. 12.7-2 traces what happens. The processor finishes the instruction it is on, saves enough information to find its way back, runs the interrupt service routine (ISR), restores what it saved, and carries on. The main program is not told any of this happened; from its point of view time simply passed.

Diagram

Fig. 12.7-2 – flow: “main program running” → “signal arrives on the interrupt pin” → “current instruction is completed” → “return address and processor state are saved” → “interrupt service routine runs” → “saved state is restored” → “main program resumes at the next instruction”, drawn as a horizontal chain with the ISR block raised above the line to show that it interrupts rather than follows the main program

Q1. That radar kept pulling the computer away from what it was doing. What lets a program drop everything the instant an event occurs, instead of waiting until it gets round to checking?

A. An interrupt. The controller’s hardware is set to watch a particular pin for a particular kind of change, and when that change occurs it suspends the running program between two instructions, runs a short routine written for the event, and then resumes exactly where it left off. Nothing in the main program has to look for the event, and no event is lost because the program happened to be busy elsewhere. The cost is the one the spacecraft met: interrupts arriving faster than the routines can be finished leave the main work starved of time, which is why an interrupt routine is kept as short as it can possibly be.

12.7.3 Attaching an Interrupt

Not every pin can raise an interrupt. On an Arduino UNO only pins 2 and 3 are wired to the external interrupt hardware, so a signal that needs an interrupt must be brought to one of those two.

attaching an interrupt service routine to a pin

attachInterrupt(digitalPinToInterrupt(pin_number), isr_name, mode);

Parts: pin_number — an interrupt-capable pin, 2 or 3 on an Arduino UNO; isr_name — the name of a function taking no arguments and returning void, written without brackets; mode — the change to watch for: RISING for LOW to HIGH, FALLING for HIGH to LOW, CHANGE for either, or LOW for as long as the pin is held low. Returns / Effect: nothing is returned. From this point on, the named routine runs whenever the specified change occurs on that pin. Example:

attachInterrupt(digitalPinToInterrupt(2), onStopOpened, RISING);

The helper digitalPinToInterrupt() converts a pin number into the interrupt number the hardware uses, and doing so makes the same line work on boards that map their pins differently. detachInterrupt(digitalPinToInterrupt(pin_number)) cancels the arrangement.

Choosing the mode is a design decision, not a formality. For an emergency stop wired as a normally-closed loop, the pin sits LOW while all is well and goes HIGH when the button is pressed or the wire breaks, so the mode is RISING. Choose FALLING and the system will announce the emergency when the button is released.

12.7.4 Writing a Safe Interrupt Service Routine

An ISR runs at a moment nobody chose, in the middle of some other calculation, with the main program frozen. Four rules follow from that, and they are not style preferences.

Keep it very short. Every microsecond spent in the ISR is taken from the main program, and while one interrupt is being serviced others are held off. Set a pin, increment a counter, record a time — then leave.

Do not use delay(), and avoid printing. delay() depends on a timer interrupt that cannot fire while your ISR is running, so it never ends. Serial.print() waits for the transmitter and can outlast several further events. Both belong in loop().

Declare shared variables volatile. A variable written by an ISR and read by loop() must be marked volatile, which tells the compiler that its value can change without any instruction in the main program having changed it.

Read multi-byte shared variables atomically. An unsigned long takes four bytes, and on an 8-bit controller reading it takes several instructions. If an interrupt lands between them, the value assembled is half old and half new. Wrapping the read in noInterrupts() and interrupts() prevents that, and the pause is a fraction of a microsecond.

Forgetting volatile on a variable shared with an ISR What students do: declare bool stopDemanded = false; as an ordinary global, set it inside the ISR and test it in loop(). What happens: the compiler can see that nothing in loop() ever changes stopDemanded, so it keeps the value in a processor register instead of re-reading memory — and may remove the test altogether. The ISR faithfully sets the variable in memory, and the main program never notices. The sketch is correct as written and wrong as compiled, which makes this fault very hard to see by reading. The fix: volatile bool stopDemanded = false;. Every variable written in an ISR and read outside it, or the reverse, carries volatile.

The station of Section 12.6 can now be given a proper emergency stop. Its stop loop moves from pin 4 to pin 2 so that it can raise an interrupt, and drive is removed inside the ISR rather than waiting for the next pass of loop().

Emergency stop captured by an interrupt Problem: remove drive from the belt within microseconds of the emergency-stop loop being broken, whatever the main program is doing at the time, and keep the station halted until the board is reset. Board and components: Arduino UNO; L298N motor driver module; the belt motor; an emergency-stop button with a normally-closed contact; one LED with a 220 Ω series resistor; a 7.4 V pack for the motor supply. Wiring: the normally-closed emergency-stop contact between pin 2 and GND, with the internal pull-up enabled. Pin 9 to ENA, pin 8 to IN1, pin 10 to IN2, OUT1 and OUT2 to the belt motor. Pack positive to +12V, pack negative to the module’s GND and to the Arduino’s GND as one common ground rail. LED anode through 220 Ω to pin 7, cathode to ground.

const byte stopPin   = 2;    // normally-closed loop: a break gives a rising edge
const int enablePin  = 9;    // ENA on the L298N
const int haltLedPin = 7;
const int in1Pin     = 8;
const int in2Pin     = 10;

volatile bool stopDemanded = false;   // written by the ISR, read by loop()
bool reported = false;                // used only inside loop()

// ISR: the urgent action only, and nothing else.
void onStopOpened() {
    digitalWrite(enablePin, LOW);
    stopDemanded = true;
}

void setup() {
    pinMode(stopPin, INPUT_PULLUP);
    pinMode(enablePin, OUTPUT);
    pinMode(haltLedPin, OUTPUT);
    pinMode(in1Pin, OUTPUT);
    pinMode(in2Pin, OUTPUT);

    digitalWrite(in1Pin, HIGH);
    digitalWrite(in2Pin, LOW);
    digitalWrite(enablePin, LOW);
    digitalWrite(haltLedPin, HIGH);

    Serial.begin(9600);
    attachInterrupt(digitalPinToInterrupt(stopPin), onStopOpened, RISING);

    // Run only if the stop loop is intact at power-on.
    if (digitalRead(stopPin) == LOW) {
        digitalWrite(enablePin, HIGH);
        digitalWrite(haltLedPin, LOW);
        Serial.println("Station running");
    }
}

void loop() {
    if (stopDemanded) {
        digitalWrite(enablePin, LOW);
        digitalWrite(haltLedPin, HIGH);

        if (!reported) {
            Serial.println("EMERGENCY STOP - reset the board to restart");
            reported = true;
        }
        return;
    }

    // The station's ordinary work would be carried out here.
}

Sample output / observed behaviour: switched on with the stop loop intact, the belt starts and the serial monitor shows

Station running

Pressing the emergency stop, or unplugging its wire, stops the belt and lights the halt indicator immediately, and one further line appears:

EMERGENCY STOP - reset the board to restart

The belt does not restart when the button is released, because stopDemanded is never cleared. Identifier description:

[Table:] Table 12.15 – identifiers used in the interrupt-driven emergency stop | Identifier | Type | Purpose | |—|—|—| | stopPin | const byte | interrupt-capable pin carrying the normally-closed stop loop | | enablePin | const int | pin driving ENA, removed by the ISR | | haltLedPin | const int | pin driving the halt indicator | | in1Pin, in2Pin | const int | pins setting the belt motor’s direction | | stopDemanded | volatile bool | flag set by the ISR to tell loop() that the stop was raised | | reported | bool | ensures the stop message is printed once, not on every pass | [/Table]

Notes: the ISR contains two statements. Removing drive is the one thing that cannot wait, so it is done there; everything else — lighting the indicator, printing, and staying halted — is left to loop(), which has all the time it needs. Compare this with the polled version in Section 12.6, where a stop pressed just after the pin was read would not take effect until the pass finished. On a station whose loop is short that difference is small; on a robot whose loop contains a distance measurement and a serial report, it is the difference between stopping and not.

Add volatile unsigned long pressCount = 0; and increment it in the ISR, then print the count from loop() whenever it changes. Press the emergency stop once, firmly, and read the count. Explain what a mechanical contact must be doing to produce the number you see.

That last exercise exposes something every interrupt-driven input suffers from. A mechanical contact does not change state once; it makes and breaks several times over a few milliseconds as the metal settles, and the interrupt hardware faithfully reports every one of those edges. The cure is to ignore further edges for a short interval after an accepted one, which needs a way of measuring time that does not stop the program. That is the subject of the next section.

12.7.5 Where Interrupts Are Used in Robots

Three families of job account for nearly all the interrupts you will meet.

Counting and timing fast edges. Encoder ticks, tachometer pulses and the returning echo of an ultrasonic sensor are all short events whose exact moment carries the information. An ISR that does nothing but increment a counter or record a timestamp can keep up with rates a polled loop cannot approach, and the readings of speed and distance built from those counts are steadier as a result.

Acting on safety inputs without delay. Emergency stops, limit switches at the end of a travel, tilt sensors and overcurrent warnings all demand a response that does not queue behind whatever the main program is doing. An ISR can drive a motor driver’s enable line low and force a robot into a safe state within microseconds.

Waking a controller that is doing nothing. A battery-powered machine can be put into a low-power state in which it draws almost no current, and be brought back by an interrupt from a button, a sensor or a timer. Nothing is polled, so nothing is spent waiting.

The temptation, once interrupts are understood, is to use them everywhere. Resist it. Every interrupt makes the program harder to reason about, because any line of loop() may now be suspended between one instruction and the next. Use an interrupt where the event is brief or the response must be immediate, and poll everything else.

12.8 Timers

12.8.1 What a Timer Is

Everything a robot does happens at a moment and lasts for a duration. A wheel turns at so many revolutions per minute, a servo expects a pulse every 20 ms, an echo returns after a certain number of microseconds, a warning light flashes twice a second. None of that is possible without a way of measuring time, and counting instructions is not one — the same line takes different times to run on different boards.

Inside the microcontroller there are dedicated pieces of hardware that do nothing but count.

NoteDefinition

A timer is a hardware counter inside a microcontroller that is increased or decreased automatically by a clock signal, independently of the program, so that intervals can be measured and periodic events produced without the processor having to wait.

The key words are independently of the program. A timer keeps counting while your code reads sensors, makes decisions and drives motors. The processor can look at the count whenever it likes, and can also ask to be interrupted when the count reaches a chosen value.

An Arduino UNO runs from a 16 MHz clock and carries three such counters, named Timer0, Timer1 and Timer2. You will use all three in this chapter without configuring any of them, because the Arduino functions you already know are built on them. That is the ordinary way of working: timers are used indirectly through millis(), micros(), delay(), analogWrite(), tone() and the servo library, and only directly when a program needs a cadence those cannot provide.

12.8.2 Timekeeping with millis() and micros()

The most useful thing a timer gives a sketch is a running count of elapsed time since the board was switched on or reset.

Table 12.16 – the timing functions of an Arduino UNO | Function | Unit | Returns | Resolution on an UNO | Wraps back to zero after | |—|—|—|—|—| | millis() | milliseconds | unsigned long | 1 ms | about 49.7 days | | micros() | microseconds | unsigned long | 4 µs | about 71.6 minutes | | delay() | milliseconds | nothing | 1 ms | — | | delayMicroseconds() | microseconds | nothing | about 1 µs | — |

Two of those entries are worth dwelling on. The resolution of micros() is 4 µs, not 1 µs, because the counter behind it advances in steps of four at this clock speed; the number returned is always a multiple of four. And both counters eventually run out of digits and start again from zero.

When millis() wraps, and why the usual subtraction survives it

Given: millis() returns a 32-bit unsigned long that increases by one every millisecond.

Find: the time until it returns to zero, and the value of now - lastEvent when lastEvent is 4,294,967,200 and now, just after the wrap, is 100.

Method: a 32-bit unsigned value holds \(2^{32}\) distinct counts. Unsigned subtraction in C is carried out modulo \(2^{32}\), so any negative result has \(2^{32}\) added to it.

\[2^{32} = 4\,294\,967\,296 \text{ ms} = 4\,294\,967.296 \text{ s} = 1193.05 \text{ h} = 49.7 \text{ days}\]

\[now - lastEvent = 100 - 4\,294\,967\,200 = -4\,294\,967\,100\]

\[-4\,294\,967\,100 + 4\,294\,967\,296 = 196\]

the counter wraps after about 49.7 days, and the subtraction across the wrap yields 196 ms.

Check: the true elapsed time is the 96 ms needed to reach the wrap plus the 100 ms after it, which is 196 ms. The arithmetic is right, and it is right only because both variables are unsigned long. Store either of them in an int or a long and the same line produces a large negative number and a robot that stops responding after seven weeks — or, with micros(), after seventy-one minutes.

This is why every timing variable in this chapter is declared unsigned long, and why intervals are always tested as a difference, now - last >= interval, rather than as an absolute comparison such as now >= last + interval.

12.8.3 Non-Blocking Scheduling Instead of delay()

delay(500) is the first timing instruction everyone learns and the first one that has to be unlearned. It does not schedule anything; it stops the program dead for half a second. Nothing else is read, nothing else is decided, and no polled input is noticed. The low-battery guard of Section 12.5.6 could afford it only because that sketch had nothing else to do.

The alternative is to look at the clock rather than to sleep. The program keeps a timestamp of when each job was last done, and on every pass asks whether enough time has gone by for that job to be due. Fig. 12.8-1 contrasts the two arrangements.

Diagram

Fig. 12.8-1 – two horizontal timelines on a shared axis marked in milliseconds. Upper timeline, labelled “with delay()”: short blocks labelled “work” separated by long shaded blocks labelled “delay(500) — nothing else can happen”, with an input event arrow landing inside a shaded block and marked “not noticed until the delay ends”. Lower timeline, labelled “with millis()”: a continuous run of short blocks labelled “loop() pass”, with two marker rows beneath it — one showing ticks every 500 ms labelled “sample the battery” and one showing ticks every 200 ms labelled “toggle the indicator” — and the same input event arrow landing between two passes and marked “noticed on the next pass”.

Because each job carries its own timestamp, jobs at quite different rates run side by side. The program below samples the pack voltage twice a second and flashes the warning indicator every 200 ms, and does neither by stopping.

A guard that samples and flashes without ever blocking Problem: read the pack voltage every 500 ms and latch a shutdown below 6.6 V; while shutdown is latched, flash the warning indicator with a 200 ms half-period; leave the controller free on every pass for the robot’s other work. Board and components: Arduino UNO; two 10 kΩ resistors forming the divider across a 7.4 V two-cell lithium polymer pack; L298N motor driver module; one LED with a 220 Ω series resistor. Wiring: divider mid-point to A0, pack negative to Arduino GND and to the module’s GND as one common ground rail. Pin 9 to ENA. LED anode through 220 Ω to pin 6, cathode to ground.

const int sensePin   = A0;
const int warnPin    = 6;
const int enablePin  = 9;

const unsigned long SAMPLE_MS = 500;    // battery sampling period
const unsigned long BLINK_MS  = 200;    // indicator half-period

const float DIVIDER_FACTOR = 2.0;
const float LOW_LIMIT      = 6.6;       // volts at the pack

unsigned long lastSampleMs = 0;
unsigned long lastBlinkMs  = 0;

bool warnState = false;
bool shutdownLatched = false;

void setup() {
    pinMode(warnPin, OUTPUT);
    pinMode(enablePin, OUTPUT);
    digitalWrite(warnPin, LOW);
    digitalWrite(enablePin, LOW);
    Serial.begin(9600);
}

void loop() {
    unsigned long now = millis();

    // Job 1: sample the pack, twice a second.
    if (now - lastSampleMs >= SAMPLE_MS) {
        lastSampleMs = now;

        int raw = analogRead(sensePin);
        float packVolts = raw * (5.0 / 1023.0) * DIVIDER_FACTOR;

        if (packVolts < LOW_LIMIT) {
            shutdownLatched = true;
        }

        if (shutdownLatched) {
            digitalWrite(enablePin, LOW);
        } else {
            digitalWrite(enablePin, HIGH);
        }

        Serial.print("Battery: ");
        Serial.print(packVolts, 2);
        Serial.println(" V");
    }

    // Job 2: flash the indicator, but only while shutdown is latched.
    if (shutdownLatched) {
        if (now - lastBlinkMs >= BLINK_MS) {
            lastBlinkMs = now;
            warnState = !warnState;
            digitalWrite(warnPin, warnState);
        }
    } else {
        digitalWrite(warnPin, LOW);
    }

    // Every other job the robot has would be carried out here, on every pass.
}

Sample output / observed behaviour: with raw readings of 700, 670 and 690 arriving on the samples at 500 ms, 1000 ms and 1500 ms, the serial monitor shows

Battery: 6.84 V
Battery: 6.55 V
Battery: 6.74 V

Drive is removed at the 1000 ms sample and stays removed, and from that instant the indicator flashes on and off every 200 ms — a full flash every 400 ms — while the samples continue undisturbed. Identifier description:

[Table:] Table 12.17 – identifiers used in the non-blocking guard | Identifier | Type | Purpose | |—|—|—| | SAMPLE_MS, BLINK_MS | const unsigned long | the period of each job, in milliseconds | | lastSampleMs, lastBlinkMs | unsigned long | when each job was last carried out | | now | unsigned long | the time at the start of this pass | | warnState | bool | the state the indicator was last set to | | shutdownLatched | bool | flag recording that the pack fell below the limit | | raw | int | the value returned by analogRead() | | packVolts | float | the pack voltage in volts | [/Table]

Notes: millis() is read once per pass into now, so both jobs are judged against the same instant. Each job updates only its own timestamp, which is why they neither drift into each other nor have to share a period. And digitalWrite(warnPin, warnState) works because HIGH is 1 and true converts to 1 — though writing it out with an if costs nothing and reads more plainly.

the indicator job on the passes that follow the latch at 1000 ms

Time now (ms) shutdownLatched now - lastBlinkMs Due? lastBlinkMs after warnState after Indicator
1000 true 1000 yes 1000 true on
1100 true 100 no 1000 true on
1200 true 200 yes 1200 false off
1310 true 110 no 1200 false off
1400 true 200 yes 1400 true on

Result: after the first toggle the indicator changes state every 200 ms, and on the passes in between the job simply falls through and costs almost nothing. Note the first row: because lastBlinkMs was still zero, the first toggle happened immediately, which is usually what is wanted from a warning.

Q2. A robot’s controller can only run one instruction at a time, yet it must watch a sensor and keep its wheels turning together. How is that managed without one job blocking the other?

A. By never letting any job wait. Each job is given a period and a timestamp of when it last ran, and loop() — which comes round many times a second — asks of each job only whether it is due. A job that is not due is skipped in a few microseconds; a job that is due does its work and records the time. Because no instruction stops the program, several jobs running at quite different rates share one controller and each appears to run on its own. The moment a delay() is used instead, every other job stops with it.

12.8.4 Timers Behind PWM, Servo and Tone

Three familiar facilities are timers wearing other names.

Pulse width modulation (PWM) produces an average voltage by switching a pin rapidly between full on and full off and varying the fraction of time it spends on, which is called the duty cycle. Fig. 12.8-2 shows the idea. A timer generates the switching, so the processor sets the duty cycle once and is then free.

Diagram

Fig. 12.8-2 – three square waveforms drawn one below the other on a common time axis, all with the same period marked T. The first is high for a quarter of each period and labelled “duty cycle 25%, analogWrite(pin, 64)”; the second is high for half and labelled “duty cycle 50%, analogWrite(pin, 128)”; the third is high for three quarters and labelled “duty cycle 75%, analogWrite(pin, 191)”. A dashed horizontal line across each waveform marks the average voltage, rising from one sketch to the next.

setting a duty cycle with analogWrite()

analogWrite(pin_number, duty_value);

Parts: pin_number — a pin marked with a tilde on the board, being 3, 5, 6, 9, 10 or 11 on an Arduino UNO; duty_value — a whole number from 0 for permanently off to 255 for permanently on. Returns / Effect: nothing is returned. The pin begins producing a square wave of fixed frequency and the stated duty cycle, and continues to do so until told otherwise. Example:

analogWrite(9, 191);    // about three-quarters of full drive

The output is not an analogue voltage, whatever the function’s name suggests. It is a rapidly switched digital signal, and a motor responds to its average because the motor cannot react fast enough to follow the switching. The switching rate on an UNO is about 490 Hz on most pins and about 980 Hz on pins 5 and 6.

The servo library uses a timer to produce the pulse train a servo motor expects — one pulse every 20 ms, whose width between roughly 1000 µs and 2000 µs sets the angle. Getting a width right to a few microseconds while the main program runs is only possible because hardware, not code, is doing the timing.

The tone() function uses a third timer to produce a square wave of a chosen frequency for a buzzer.

Table 12.18 – what each timer of an Arduino UNO serves | Timer | Serves | Consequence for a sketch | |—|—|—| | Timer0 | millis(), micros(), delay(), and PWM on pins 5 and 6 | reconfiguring it breaks all timekeeping | | Timer1 | PWM on pins 9 and 10, and the servo library | attaching a servo stops PWM working on pins 9 and 10 | | Timer2 | PWM on pins 3 and 11, and tone() | using tone() stops PWM working on pins 3 and 11 |

Expecting a servo and PWM on the same timer’s pins What students do: attach a servo with the servo library and drive a motor’s speed with analogWrite(9, …) on the same board. What happens: the servo library takes over Timer1 to generate its pulses, and pins 9 and 10 stop producing PWM. The motor runs at full speed or not at all, and the sketch contains no mistake at all — the conflict is in the hardware the two facilities share. The fix: move the speed control to a pin served by a different timer, such as 5 or 6, and check Table 12.18 before assigning pins.

12.8.5 Measuring Pulse Width and Frequency

Timers also work in the other direction: instead of producing an interval, they measure one.

The function pulseIn(pin, value, timeout) waits for the pin to reach the stated level, times how long it stays there, and returns that duration in microseconds as an unsigned long. It returns 0 if nothing happens before the timeout. This is how an ultrasonic sensor is read: a short trigger pulse is sent, and the width of the echo pulse that comes back is the round-trip travel time of the sound.

unsigned long duration = pulseIn(echoPin, HIGH, 25000);   // give up after 25 ms

Turning an echo pulse into a distance

Given: an echo pulse of width 1160 µs; the speed of sound in air taken as 343 m/s at about 20 °C.

Find: the distance to the object, in centimetres.

Method: 343 m/s is 0.0343 cm/µs. The pulse travels to the object and back, so the one-way distance is half the total.

\[d_{\text{total}} = 0.0343 \text{ cm/µs} \times 1160 \text{ µs} = 39.79 \text{ cm}\]

\[d = \frac{39.79 \text{ cm}}{2} = 19.9 \text{ cm}\]

the object is about 19.9 cm away.

Check: this is just below the 20 cm threshold of the trolley in Section 12.2, so that trolley would stop — which is the right order of magnitude for an aisle obstacle. Note the assumption: the speed of sound rises by roughly 0.6 m/s for every degree the air warms, so a sensor calibrated in a cool room reads slightly long in a hot workshop.

Frequency is measured the other way round: rather than timing one pulse, the program counts pulses over a known window. An interrupt increments a counter, as in Section 12.7, and a millis()-based job like the ones above samples and clears that counter exactly once a second. Dividing the count by the window length gives pulses per second, and dividing that by the number of pulses per revolution gives the shaft speed. Both halves of the measurement — the counting and the window — are the work of timers, and neither requires the main program to wait for anything.

12.9 Testing and Debugging

12.9.1 What Testing Is

A sketch that compiles has passed exactly one test: the compiler could understand it. Nothing about a successful upload says the machine does what it was built to do.

NoteDefinition

Testing is the process of checking, under stated conditions, whether a program and the hardware it controls behave as the specification requires.

Two words in that sentence carry the weight. Testing is checked against a specification, which is why Section 12.1 insisted on writing one; without it there is nothing to be right or wrong about, and a test becomes an opinion about whether the robot looks all right. And it is checked under stated conditions, because a machine that behaves correctly on a white bench in daylight may not do so on a dark floor.

A test is planned before it is run, and the expected result is written down first. Deciding what should happen after seeing what did happen is not testing; it is agreeing with the machine.

Three kinds of condition deserve a place in every test schedule. Ordinary cases confirm the everyday behaviour. Boundary cases probe the exact value at which behaviour changes, which is where comparison operators go wrong. Invalid cases supply what the sensor should never send — no echo at all, a disconnected wire, a reading of zero — because those are the cases the program was written without thinking about.

Table 12.19 – a test schedule for the trolley of Section 12.2, showing the condition, the behaviour required, and how it is checked | Kind | Condition applied | Behaviour required | How it is checked | |—|—|—|—| | Ordinary | a wall placed 45 cm ahead | both wheels drive forward | observe; confirm the printed distance is near 45 | | Ordinary | a wall placed 8 cm ahead | wheels stop, then the trolley turns right | observe one full stop-and-turn cycle | | Boundary | a board fixed at exactly 20 cm | wheels stop | measure the gap with a rule before the run | | Boundary | a board at 21 cm | wheels continue to drive | measure the gap; repeat five times for consistency | | Invalid | sensor aimed into open space | treated as clear; the trolley drives on | confirm the printed distance is the out-of-range value | | Invalid | a cushion held 10 cm ahead | the reading is unreliable; note what happens | record the behaviour honestly rather than excusing it |

The last row is the one students are tempted to leave out. A soft surface scatters the ultrasonic pulse, so the trolley may drive straight into it. That is a genuine limitation of the sensor, and a test schedule exists to reveal such things, not to award the machine a pass.

12.9.2 What Debugging Is

Testing tells you that something is wrong. It rarely tells you what.

NoteDefinition

Debugging is the process of locating the cause of a fault revealed by testing, correcting it, and confirming that the correction has worked.

Debugging is detective work, and it has a different rhythm from testing. Testing is planned in advance and run to a schedule; debugging is a hunt that narrows, one measurement at a time, until the cause is cornered.

Table 12.20 – testing and debugging compared | Feature | Testing | Debugging | |—|—|—| | Question it answers | does the machine meet its specification? | why does it not, and where? | | When it happens | after any change, to a written schedule | after a test has failed | | What it needs to begin | a specification and a plan | a fault that can be reproduced | | What it produces | a pass or a fail, and evidence | an identified cause and a correction | | Frame of mind | sceptical and systematic | narrowing and evidence-led | | When it ends | when every planned case has been run | when the fault can be made to appear and disappear at will |

The last row of that table is a standard worth holding to. A fault is not understood until you can switch it on and off deliberately. Until then, what you have is a change that seems to have helped — and changes that seem to have helped come back.

Q. “It worked yesterday and nothing has been changed, but today it does not work.” What is wrong with that sentence?

A. The last four words. Something always changed. The pack has discharged and sags further under load; the daylight through the window is stronger and is saturating an infrared sensor; a jumper has been pulled half out of the breadboard; a screw terminal has worked loose with vibration; the surface under the robot has been swept and reflects differently. Treat “nothing changed” as the first hypothesis to be disproved, and start with the battery voltage and the ground connections.

12.9.3 Methods of Testing and Debugging

A robot fault may lie in the logic, in the wiring, or in the meeting point between them. Five methods cover almost everything, and they are chosen for the kind of fault suspected.

Serial monitor reporting. The board prints what it is doing to the computer over the serial link. Print the raw reading as well as the converted value, and print the decision as well as the action — the interesting mistake is usually between the two. This is the most powerful method available in a school laboratory, and it costs two lines of code.

Indicator LEDs. Where the robot must run untethered, a printed message is useless. An LED lit at a chosen point in the program says whether that point was reached; two LEDs say which branch was taken. Lighting an indicator inside an interrupt service routine is legitimate, where printing from one is not.

Incremental and modular testing. Build and prove one subsystem at a time. Confirm the sensor reads sensibly before the motors are attached; confirm the motors run before the logic is added. A fault found in a five-line sketch has five lines to hide in; the same fault in the finished robot has a hundred.

Instrument measurement. A multimeter answers questions no amount of code can: whether the pack is really at 7.4 V under load, whether the divider mid-point is where it should be, whether the ground rail is continuous, whether the pin the code drives is actually changing. Fig. 12.9-1 marks the points worth probing on a typical drive chain.

In-circuit debugging and breakpoints. Specialised tools halt the processor at a chosen instruction so that variables can be inspected and the program stepped forward one line at a time. This is how professional embedded work is done. It is not usually available on a classroom Arduino UNO, which is precisely why serial reporting matters so much there.

Diagram

Fig. 12.9-1 – block diagram of a drive chain drawn left to right: battery pack → L298N motor driver → motor, with a separate block for the Arduino UNO whose signal arrows go to the driver’s ENA and IN1/IN2 inputs. Four numbered test points marked with probe symbols: (1) across the pack terminals, labelled “measure under load, not at rest”; (2) at the driver’s supply terminal, labelled “confirms the supply reaches the module”; (3) at the Arduino output pin driving ENA, labelled “confirms the controller is asking”; (4) across the motor terminals, labelled “confirms the driver is delivering”. A heavy line joining pack negative, module GND and Arduino GND labelled “common ground — check continuity first”.

Reading that figure the right way round is the whole method. Work along the chain from the supply towards the load, and the first test point that reads wrongly is where the fault lies — everything upstream of it is proved good, and everything downstream is not yet in question.

Q4. A program is running inside a sealed board with no screen of its own. How can you find out what it is thinking while it runs?

A. by making it report. Serial.begin() opens a link to the computer, and Serial.print() sends whatever you ask for — a raw sensor reading, a converted value, the state of a flag, the branch just taken — to the serial monitor as the program runs. Where the robot must run untethered, an indicator LED lit at a chosen point does the same job with one bit of information. And where the question is electrical rather than logical, a multimeter reads the actual voltage at a pin and settles the matter that no amount of code inspection can. The principle in all three is the same: a program’s thinking is invisible only until you instruct it to be visible.

Before any logic is blamed, the hardware should be proved. The sketch below is a bench test, not part of any robot: it steps the drive duty upwards a little at a time and reports each value, so that the duty at which the wheels actually begin to turn can be found and recorded.

The chassis is placed on blocks with its wheels clear of the bench before this sketch is run, and it stays there for the whole test. Fingers, sleeves and hair are kept away from the wheels and the drive shaft. Power is disconnected before any wire is moved and reconnected only after the wiring has been rechecked.

A bench test that finds the duty value at which the wheels begin to turn Problem: raise the drive duty cycle in steps of 15, holding each step for one second and reporting it, so that the smallest duty value that actually turns the loaded wheels can be observed and written down. Board and components: Arduino UNO; L298N motor driver module; one drive motor on the chassis; a 7.4 V pack for the motor supply. The chassis is supported on blocks. Wiring: pin 9 to ENA, pin 8 to IN1, pin 10 to IN2; OUT1 and OUT2 to the motor. Pack positive to the module’s +12V terminal; pack negative, the module’s GND and the Arduino’s GND joined as one common ground rail. The board is powered from USB so that the serial monitor can be read.

const int enablePin = 9;      // ENA, a PWM-capable pin
const int in1Pin    = 8;
const int in2Pin    = 10;

const unsigned long STEP_MS = 1000;   // one second at each duty value
const int STEP_SIZE = 15;

int duty = 0;
unsigned long lastStepMs = 0;

void setup() {
    pinMode(enablePin, OUTPUT);
    pinMode(in1Pin, OUTPUT);
    pinMode(in2Pin, OUTPUT);

    digitalWrite(in1Pin, HIGH);       // one fixed direction throughout
    digitalWrite(in2Pin, LOW);
    analogWrite(enablePin, 0);

    Serial.begin(9600);
    Serial.println("Duty test - chassis must be on blocks");
}

void loop() {
    unsigned long now = millis();

    if (now - lastStepMs >= STEP_MS) {
        lastStepMs = now;

        analogWrite(enablePin, duty);
        Serial.print("Duty: ");
        Serial.println(duty);

        duty = duty + STEP_SIZE;
        if (duty > 255) {
            duty = 0;                 // start the sweep again
        }
    }
}

Sample output / observed behaviour: the monitor shows

Duty test - chassis must be on blocks
Duty: 0
Duty: 15
Duty: 30
Duty: 45
Duty: 60
Duty: 75
Duty: 90

On the chassis this was written for, the wheel remained still up to 60, began to turn unevenly at 75, and ran smoothly from about 105 upwards. That first figure is worth recording, because a robot commanded to creep at a duty of 40 will sit still while its program insists it is driving. Identifier description:

[Table:] Table 12.21 – identifiers used in the duty bench test | Identifier | Type | Purpose | |—|—|—| | enablePin | const int | the PWM pin driving ENA on the L298N | | in1Pin, in2Pin | const int | the direction pair, set once and left alone | | STEP_MS | const unsigned long | how long each duty value is held | | STEP_SIZE | const int | the increase applied at each step | | duty | int | the duty value now being applied, 0 to 255 | | lastStepMs | unsigned long | when the duty was last increased | [/Table]

Notes: the direction pins are set once in setup() so that only one quantity changes during the test — which is the rule the whole of Section 12.9.5 is built on. The sweep restarts rather than stopping, so the threshold can be watched two or three times before the pack is disconnected.

12.9.4 Errors That Are Made Again and Again

Most faults in a school robot are drawn from a short list, and a good deal of time is saved by working from the symptom rather than from suspicion. Table 12.22 is written in that direction.

Table 12.22 – common symptoms in a robot, with the causes worth checking first, in the order worth checking them | Symptom | Causes to check, in order | |—|—| | Nothing happens at all when power is applied | supply not reaching the board; sketch not actually uploaded; the whole method placed in setup() | | An output device never responds | wrong pin number in the code; pinMode() missing or set to INPUT; the load drawing its current through a logic pin | | A sensor reading jumps about at random | input left floating with no pull-up; no common ground between the modules; a loose jumper | | The robot does the opposite of what is intended | active-low signal read as though it were active-high; the two branches of an if interchanged | | A condition is never satisfied | = written where == was meant; the comparison made against the raw reading when the converted value was intended | | A count is far too low | a brief event polled instead of caught by an interrupt | | A count is two or three times too high | a mechanical contact bouncing; CHANGE used where one edge per pulse was wanted | | A shared value never seems to change | a variable written in an interrupt service routine and not declared volatile | | A timing value behaves oddly after half a minute | a millis() value stored in an int | | The motor runs but its speed will not change | the enable line on a pin whose timer has been claimed by another library | | The board resets whenever the motor starts | the motor supply drawn through the board; the pack sagging under load; grounds joined at more than one point | | It works on the bench and fails on the floor | reflectance of the real surface; pack voltage lower under load; a longer loop() once every subsystem is running |

Two entries in that table are worth reading twice, because they explain faults that look like broken hardware. A board that resets when the motor starts is almost never a faulty board — it is a supply problem. And a machine that behaves differently on the floor from on the bench has not become unreliable; it is being tested, for the first time, under the conditions it was actually built for.

12.9.5 A Systematic Way to Find a Fault

Faced with a robot that misbehaves, the natural response is to change something and see. It is also the slowest possible method, because two changes made together can cancel, and a fault that disappears for unknown reasons will return.

The procedure below is the one to follow instead. It is slower for the first two minutes and very much faster after that.

Locating a fault in a robot Input: a machine that fails a stated test, and the specification it was supposed to meet. Output: an identified cause, a correction, and evidence that the correction works. Method:

1. Write down exactly what was expected and exactly what happened.
2. Make the fault appear again, on demand. If it cannot be reproduced,
   record the conditions under which it appeared and continue testing
   until it can be.
3. Decide whether the fault is electrical or logical: measure the supply,
   the ground continuity and the signal at the pin concerned.
4. If it is electrical, work along the chain from supply to load and stop
   at the first test point that reads wrongly.
5. If it is logical, print the values the decision is made from, not only
   the action taken.
6. Reduce the problem: run the smallest sketch, or the smallest circuit,
   that still shows the fault.
7. Change exactly one thing. Test. Write down the result.
8. Repeat step 7 until the fault is located.
9. Correct it, then restore the original fault deliberately and confirm
   that it returns.
10. Run the whole test schedule again, not only the case that failed.

Why it works: every step either halves the region the fault can be hiding in or turns an opinion into a measurement. Step 7 is what makes the search valid — with one variable changed at a time, the result of each test means something. Step 9 is what turns a guess into an explanation, because a cause you can switch on again is a cause you have understood. Watch out: step 10 is the one that gets skipped, and a correction that breaks a case which used to pass is a common and demoralising way to lose an afternoon. Note also that steps 3 and 6 are worth doing early rather than late; a great deal of code has been rewritten to cure a loose ground wire.

Keeping a short written record while you work — what was changed, and what happened — feels unnecessary at the time and is invaluable twenty minutes later, when three things have been altered and the machine is behaving worse than when you started.

Finally, correcting a fault is not the end of the work. Ask why the fault was possible. A pin number that appeared twice in the sketch and was changed in only one place is not merely a typing slip; it is a reason to declare pin numbers once, as a constant, so that the same mistake cannot be made again. Programs are made steadily more reliable in that way — not by finding faults faster, but by removing the conditions that allow them.

12.10 Summary

From a problem to a working machine
  1. A controller carries out its instructions one at a time, in the order written, exactly as written. It has no notion of what was intended, so a wrong sign or a wrong pin number produces a machine that behaves confidently and wrongly.
  2. Three kinds of instruction must be supplied before any robot works: configuration, which tells the board what each pin is for; initialisation, which is done once at switch-on; and the repeating work of sensing, deciding and acting.
  3. Every robot program has the shape of the sense–plan–act loop. The controller never touches the world directly: what reaches it is a voltage on a pin, and what leaves it is a voltage on a pin. Because the robot’s own action changes what will be sensed next, the reading must be taken freshly on every pass.
  4. A digital signal has two meaningful states, HIGH and LOW. An analogue signal may take any voltage between the limits, and its exact value carries the information.
  5. Work proceeds through settled stages: a specification, then the method in words, then the method as a picture, then the method line by line, then the program, then testing and correction. A specification is written precisely enough that someone else could test the machine against it without asking a question — it names what is sensed, what threshold separates the cases, what is actuated, and what happens at the boundary.
Algorithms
  1. An algorithm is a finite, ordered sequence of unambiguous steps which, carried out on given inputs, produces the required result or behaviour. It is tied to no programming language.
  2. Five properties make an algorithm usable: finiteness, definiteness, input, output and effectiveness.
  3. Finiteness applies to one pass, not to the machine’s working life. Each journey through the method must end; repeating a finished pass for as long as power lasts is a design decision, not a violation.
  4. Effectiveness is the property most often lost, by writing a wish rather than an operation. “Check whether the path is safe” is a wish; “measure the distance ahead in centimetres and compare it with 20” is an operation.
  5. Four properties of the finished machine are settled in the algorithm and nowhere else: what counts as a reason to act, the order that prevents conflicting commands, what happens when a reading is invalid, and how much work is repeated.
  6. How often the method senses decides how far the robot travels unaware. Over one pass at steady speed, \(d = v \times t\), so a pass lengthened carelessly moves the machine closer to whatever it was avoiding.
Flowcharts
  1. A flowchart is a diagrammatic representation of an algorithm, in which standard symbols show the steps and decisions and arrows show the order in which control passes.
  2. The five standard symbols are the oval for a terminal, the rectangle for a process, the diamond for a decision, the parallelogram for input or output, and the arrow, or flowline, for the direction of control.
  3. A parallelogram is used where data crosses the boundary of the system; a rectangle is used where the machine acts on what it already has. A diamond holds a question, never an instruction, and each of its two exits is labelled and leads somewhere meaningful.
  4. Almost every robot chart is built from two shapes: a loop with no exit, for continuous control, and a loop with an exit governed by a value the machine maintains for itself.
  5. A sensor reports a state, not an event. Counting must therefore be triggered by the change from one state to the other, or a single object will be counted many times.
Pseudocode
  1. Pseudocode is a description of a method written in the structure of a program but in ordinary language, following no language’s syntax rules. Nothing in it can fail to compile, so every mistake found in it is a mistake of thinking.
  2. Keywords are written in capitals, one action to a line, and indentation shows what belongs inside what. SET stores a value, READ and DISPLAY carry data across the system boundary, IF … ELSE chooses, and REPEAT CONTINUOUSLY, FOR and WHILE repeat.
  3. END IF marks where a choice finishes and ordinary sequence resumes. Without it, a reader cannot tell whether the following line belongs to a branch or runs in every case, and those two readings produce different machines.
  4. Pseudocode lines map closely onto program instructions, and the repeating block maps onto the region the board runs over and over.
Programming a microcontroller
  1. Embedded C is the C language extended to work directly with microcontroller hardware. It exists because such a board has no operating system to ask, no memory to waste, and a program worth carrying to the next board.
  2. The Arduino functions are ordinary prewritten code built on embedded C. They make a program readable and portable, at the cost of a few microseconds per call — a cost that is irrelevant beside a delay() and decisive in a signal generated tens of thousands of times a second.
  3. A sketch has five regions in a fixed order: library inclusion, global constants and variables, user-defined functions, setup() and loop(). setup() runs once at power-on or reset; loop() runs again and again for as long as power lasts.
  4. A variable’s type fixes the memory it occupies and the values it can hold. On an Arduino UNO an int holds two bytes, so a millisecond count stored in one becomes meaningless after about half a minute; timestamps are held in an unsigned long.
  5. A digital pin must be configured before use. INPUT_PULLUP holds a pin reliably high through an internal resistor so that a switch can pull it low; an input left floating has no defined voltage and returns readings that follow the interference around it.
  6. A pin carries a signal, never a load. An Arduino UNO pin is rated for 20 mA in normal use and 40 mA absolute maximum, so motors, lamps, relays and solenoids are switched by a driver taking its own supply from the battery.
  7. The converter on an Arduino UNO produces a whole number from 0 to 1023 across the range from 0 V to the reference voltage, giving a step of about 4.89 mV with a 5 V reference. A reading is converted with \(V = \text{raw} \times V_{\text{ref}} / 1023\), and a divider ratio is undone by multiplication.
  8. A function is a named block that does one job, written once and called wherever needed, so that a later correction is made in one place.
  9. A microcontroller and a single-board computer are different tools. The microcontroller starts instantly, keeps strict timing, offers analogue inputs and 5 V logic, and survives having its power cut. The single-board computer runs an operating system, is far better at vision, networking and heavy computation, works at 3.3 V, and can be damaged by a 5 V signal or an abrupt loss of power.
Decision-making with Boolean operators
  1. A condition is a question whose answer is true or false, built with the comparison operators ==, !=, <, >, <= and >=. The difference between < and <= decides the machine’s behaviour at exactly the threshold value.
  2. The three Boolean operators are AND, OR and NOT. AND (&&) is true only when both conditions hold and is the operator of permission. OR (||) is true when at least one holds and is the operator of alarm. NOT (!) reverses a single condition.
  3. && is logical and & is bitwise. With the values 1 and 0 they agree, which is why the mistake survives testing; with other values they do not.
  4. AND short-circuits: if the left condition is false the right is not evaluated, which matters when the right-hand side does something as well as being read. ! binds more tightly than && and ||, so brackets are compulsory when a whole expression is to be inverted.
  5. Each raw level is converted once into a named fact, and the rule is then written in facts rather than voltages. Change the wiring and one line changes.
  6. A signal whose active state is the low one is called active-low. Switches wired to ground with a pull-up are active-low by necessity; sensor modules may be either, and which one must be established before the logic is written.
  7. A running permission is re-decided on every pass; a latched condition is set once and does not clear itself. A guard interlock needs the first; a battery guard needs the second, because removing the load lets the pack recover and would otherwise restart the machine.
  8. An emergency stop is wired as a normally-closed loop, so that a cut wire produces the same result as a pressed button. A software interlock is a convenience, never a safety system; a machine that can injure carries a hardware stop that breaks the motor supply itself.
Interrupts
  1. Polling means examining an input whenever the program reaches that line. It suits slow states and misses anything that begins and ends between two looks, without any indication that something has been missed.
  2. An interrupt is a hardware signal that suspends the main program at the end of the current instruction, transfers control to a short routine, and resumes the main program at exactly the point of suspension. The routine is the interrupt service routine, and the hardware calls it, not the program.
  3. On an Arduino UNO only pins 2 and 3 are interrupt-capable. The trigger modes are RISING, FALLING, CHANGE and LOW; RISING and FALLING give one trigger per pulse at opposite ends of the event, and CHANGE gives two.
  4. An interrupt service routine is kept very short, never uses delay() and avoids printing, because further interrupts are held off while it runs — including the timer that maintains the millisecond count, which cannot advance there.
  5. A variable shared between a routine and the main program is declared volatile, or the compiler may keep a stale copy in a register. A shared variable of more than one byte is copied with interrupts briefly disabled, so that the copy cannot be assembled half from the old value and half from the new.
  6. Interrupts are used for counting and timing fast edges, for acting on safety inputs without delay, and for waking a sleeping controller. Everything slow is still polled, because each interrupt makes the program harder to reason about.
Timers
  1. A timer is a hardware counter inside the microcontroller, increased automatically by a clock signal independently of the program, able to raise an interrupt on reaching a chosen value or overflowing. Because it is a circuit and not a variable, timing based on it does not drift when the program becomes busy.
  2. millis() and micros() return the elapsed time since power-on as an unsigned long. The microsecond count returns to zero after about 70 minutes and the millisecond count after about 49.7 days.
  3. Elapsed time is always tested as a difference, now - lastEventMs >= PERIOD, never as an absolute comparison, because unsigned subtraction gives the correct interval even across the moment the counter wraps.
  4. Scheduling with millis() lets several jobs at different rates share one controller: each keeps its own timestamp, jobs not yet due are skipped in microseconds, and the loop comes round thousands of times a second. delay() spends the same interval doing nothing at all.
  5. A bouncing mechanical contact is dealt with by recording the time of the accepted edge and refusing any further edge arriving within about 50 ms of it.
  6. Pulse width modulation switches an output on and off rapidly, and the proportion of each cycle for which it is high is the duty cycle. analogWrite() takes a value from 0 to 255 and the timer then maintains the waveform in hardware, so the average delivered is \(V_{\text{avg}} = (\text{duty}/255) \times V_{\text{supply}}\). Motor speed is not proportional to duty at the low end, where friction holds the shaft still.
  7. A board has only a few timers and they are shared. On an Arduino UNO, Timer0 maintains millis(), micros() and delay(); Timer1 drives PWM on pins 9 and 10 and is claimed by the Servo library; Timer2 drives PWM on pins 3 and 11 and is claimed by tone(). A library that takes a timer takes with it whatever else that timer provided.
  8. pulseIn() measures the width of a pulse in microseconds and is always given a timeout, since a robot waiting for an echo that never arrives has stopped sensing everything else. An ultrasonic reading becomes a distance through \(d = t \times 0.0343 / 2\) in centimetres, the division by two because the pulse travelled out and back; the constant rises slightly with temperature.
  9. Frequency is measured by counting edges with an interrupt and sampling the count on a timed schedule — the two ideas working together.
Testing and debugging
  1. Testing is checking, under stated conditions, whether the program and its hardware behave as the specification requires. The expected result is written before the test is run; deciding afterwards what should have happened is agreeing with the machine.
  2. A schedule covers ordinary cases, boundary cases at the exact value where behaviour changes, and invalid cases such as a missing echo or a disconnected wire. A test exists to reveal a machine’s limitations, not to award it a pass.
  3. Debugging is locating the cause of a fault revealed by testing, correcting it, and confirming the correction. A fault is not understood until it can be made to appear and disappear at will.
  4. The methods available are serial monitor reporting, indicator LEDs, incremental and modular testing, instrument measurement with a multimeter, and in-circuit debugging with breakpoints. Printing is impossible inside an interrupt service routine; lighting an indicator there is not.
  5. Electrical faults are found by working along the chain from supply to load — pack under load, driver supply, controller output pin, motor terminals — and the first test point that reads wrongly locates the fault. Ground continuity is checked first.
  6. Faults are searched for symptom-first. A board that resets when the motor starts is a supply problem, not a faulty board; a machine that works on the bench and fails on the floor is being tested for the first time under the conditions it was built for.
  7. The procedure is to record what was expected and what happened, reproduce the fault on demand, separate electrical from logical, reduce the problem to the smallest case that still shows it, change exactly one thing at a time, restore the fault deliberately to confirm the cause, and then run the whole schedule again.
  8. Correcting a fault is not the end of the work. Asking why the fault was possible — and removing the condition that allowed it, such as a pin number written in two places instead of declared once — is what makes programs steadily more reliable.

12.11 Key Terms

  1. active-low — a signal whose meaningful or active state is the low one, as with a switch wired between a pin and ground while an internal pull-up holds the pin high.
  2. algorithm — a finite, ordered sequence of unambiguous steps which, carried out on given inputs, produces the required result or behaviour.
  3. analogue — a signal that may take any voltage between the supply limits, and whose exact value carries the information.
  4. Boolean — having only the two values true and false; the Boolean operators AND, OR and NOT combine such values into a single decision.
  5. breakpoint — a point in a program at which a debugging tool halts execution so that variables can be inspected and the program stepped forward one instruction at a time.
  6. condition — a question written into a program whose answer is either true or false, and on which an if statement acts.
  7. configuration — the instructions that tell a board what each pin is to be used for, given before any pin is read or driven.
  8. counter — a variable, or a hardware register, that records how many times an event has occurred.
  9. DC — direct current; current that flows in one direction only, as supplied by a battery.
  10. debugging — the process of locating the cause of a fault revealed by testing, correcting it, and confirming that the correction has worked.
  11. definiteness — the property of an algorithm by which every step has exactly one possible meaning.
  12. digital — a signal having only two meaningful states, read as HIGH or LOW.
  13. embedded C — the C programming language extended with the features needed to work directly with microcontroller hardware, such as pins, timers, interrupts and memory registers.
  14. ENA — the enable input of one channel of an L298N motor driver module; holding it low removes drive from that channel.
  15. finiteness — the property of an algorithm by which one pass through it ends after a definite number of steps.
  16. flag — a variable, usually of type bool, that records that something has happened so that another part of the program can act on it later.
  17. flowchart — a diagrammatic representation of an algorithm in which standard symbols show the steps and decisions, and arrows show the order in which control passes.
  18. global variable — a variable declared outside every function, and therefore usable anywhere in the sketch.
  19. GND — the ground, or zero-volt, connection of a circuit; every board and module in one machine shares a single common ground.
  20. initialisation — the work a program does once, at switch-on, such as starting a serial link or putting an actuator into a known safe state.
  21. interrupt — a hardware signal that suspends the main program at the end of the instruction being executed, transfers control to a short routine written for the event, and then resumes the main program at exactly the point of suspension.
  22. interrupt service routine (ISR) — the short function that runs when an interrupt occurs; it is called by the hardware and never by the program.
  23. ISR — interrupt service routine.
  24. iteration — one complete pass through a loop.
  25. LED — light-emitting diode; a component that emits light when a small current passes through it in one direction, always used with a series resistor.
  26. library inclusion — the region at the top of a sketch where prewritten collections of code are brought in with #include.
  27. polling — examining an input by reading it whenever the program next reaches that instruction.
  28. program — an ordered set of instructions, stored in a controller’s memory, that the controller carries out one at a time to produce a required behaviour.
  29. pseudocode — a description of a method written in the structure of a program but in ordinary language, following no programming language’s syntax rules.
  30. pulse width modulation (PWM) — switching an output rapidly between on and off, the proportion of each cycle for which it is on being the duty cycle, so that the average delivered to the load can be varied without wasting energy as heat.
  31. PWM — pulse width modulation.
  32. repeating work — the sensing, deciding and acting that a robot performs over and over, placed in loop().
  33. SBC — single-board computer; a complete computer on one board, running an operating system, of which the Raspberry Pi is the usual example.
  34. Servo library — the prewritten code supplied for driving servo motors, which claims Timer1 on an Arduino UNO and so disables PWM on pins 9 and 10.
  35. specification — a statement of required behaviour, written precisely enough that another person could test the finished machine against it without asking a question.
  36. testing — the process of checking, under stated conditions, whether a program and the hardware it controls behave as the specification requires.
  37. timer — a hardware counter inside the microcontroller, increased automatically by a clock signal independently of the program, which can raise an interrupt on reaching a chosen value or on overflowing.
  38. USB — universal serial bus; the connection through which a sketch is uploaded to the board and through which the serial monitor communicates with it.
  39. user-defined function — a named block of code, written by the programmer, that performs one job and may be called from anywhere in the sketch.
  40. volatile — the keyword applied to a variable whose value can change without any instruction of the main program having changed it, compelling the compiler to re-read it from memory each time it is used.

12.12 Objective Questions

  1. On an Arduino UNO, a call to analogRead() returns a whole number in the range
  1. 0 to 255
  2. 0 to 511
  3. 0 to 1023
  4. 0 to 4095
  1. A sensor produces one clean pulse each time a component passes it. Which trigger mode would cause the interrupt service routine to run twice for every component?
  1. RISING
  2. FALLING
  3. CHANGE
  4. LOW
  1. Assertion (A): A variable written inside an interrupt service routine and read in loop() is declared volatile. Reason (R): The keyword volatile prevents an interrupt from occurring while that variable is being read.
  1. Both A and R are true, and R is the correct explanation of A.
  2. Both A and R are true, but R is not the correct explanation of A.
  3. A is true but R is false.
  4. A is false but R is true.
  1. Assertion (A): delay() must never be used inside an interrupt service routine. Reason (R): The millisecond count that delay() waits on is maintained by a timer interrupt, which cannot run while an interrupt service routine is executing.
  1. Both A and R are true, and R is the correct explanation of A.
  2. Both A and R are true, but R is not the correct explanation of A.
  3. A is true but R is false.
  4. A is false but R is true.
  1. Match each flowchart symbol in Column A with what it is permitted to hold, in Column B.
Column A Column B
a. Oval i. One deterministic action
b. Rectangle ii. Data crossing the boundary of the system
c. Diamond iii. The entry point and any exit point
d. Parallelogram iv. A question with two labelled exits
  1. Fill in the blanks.
  1. Elapsed time is tested as a ______ of two millis() values, so that the comparison remains correct after the counter has wrapped.
  2. An input pin with nothing connected to it and no pull-up enabled is said to be ______.
  3. A signal whose meaningful state is the low one is described as ______.
  1. State whether each of the following is true or false.
  1. A decision symbol in a flowchart may contain an instruction such as “Read the sensor”.
  2. A digital pin of an Arduino UNO may supply the current for a BO motor provided the duty cycle is kept low.
  3. Attaching a servo on an Arduino UNO can stop analogWrite() from working on pins 9 and 10.
  1. Give the full form of each of the following: ISR, PWM, SBC.

12.13 Check Your Concepts

  1. Define an algorithm, and name its five properties.

  2. A line-following robot’s program runs from the moment it is switched on until its battery is exhausted. Explain, with reference to the finiteness property, why this does not disqualify its method from being an algorithm. State one thing that would.

  3. Differentiate between polling and an interrupt as ways of noticing an input. For each, give one situation in a robot where it is the better choice, with a reason.

  4. A microswitch is wired between a digital pin and ground, with INPUT_PULLUP enabled. Explain what the pin reads when the switch is open and when it is closed, why the arrangement is described as active-low, and how the program should be written so that the rest of the code can be read without thinking about voltages.

  5. A slotted disc with 24 slots is fitted to a shaft turning at 900 rpm. Calculate the number of pulses produced each second and the duration of one pulse period. A program whose loop() takes 30 ms reads the sensor pin once per pass. State, with justification, whether this program can count the pulses correctly, and what should be done instead.

  6. A machine has two protective conditions: a guard flap that must be closed for the belt to run, and a battery pack that must not be discharged below its limit. One of these should be re-decided on every pass and the other should latch until the machine is reset. State which is which, justify each choice, and describe one consequence of getting each of them the wrong way round.

  7. A wheeled robot must measure the distance ahead every 100 ms, change the state of a status indicator every 400 ms, count encoder pulses arriving about 500 times a second, and remove drive immediately when an emergency stop is opened. Explain how you would arrange the program so that all four are achieved, naming the mechanism used for each and giving a reason for each choice.

12.14 Diagram, Circuit & Output Reading

  1. Study the fragment below, which compiles and runs on an Arduino UNO.
int count = 0;

void setup() {
    Serial.begin(9600);
}

void loop() {
    count = count + 3;

    if (count > 10) {
        Serial.println(count);
        count = 0;
    }

    delay(100);
}
  1. State the value printed on the first line of the serial monitor.
  2. State how many passes of loop() occur between one printed line and the next.
  3. State the time between consecutive printed lines, and show how you obtained it.
  1. A lamp is to be lit while a push button, wired between pin 4 and ground, is held down. The fragment below compiles without any error, uploads, and the lamp never lights.
const int SWITCH_PIN = 4;
const int LAMP_PIN = 9;

void setup() {
    pinMode(SWITCH_PIN, INPUT_PULLUP);
    pinMode(LAMP_PIN, OUTPUT);
}

void loop() {
    int state = digitalRead(SWITCH_PIN);

    if (state = LOW) {
        digitalWrite(LAMP_PIN, HIGH);
    } else {
        digitalWrite(LAMP_PIN, LOW);
    }
}
  1. Identify the single fault.
  2. Explain why the condition can never be satisfied.
  3. Write the corrected line.
  1. A student sets up an infrared beam-break module to count components. The module’s VCC goes to the 5V pin of the Arduino UNO and its OUT goes to pin 2, to which an interrupt is attached on FALLING. The module’s GND is taken to the negative terminal of a separate 6 V pack that powers nothing else in the circuit. The board is powered from USB.
  1. Identify the single fault in this arrangement.
  2. Describe the behaviour the student is likely to observe from the count.
  3. State the correction, and name the instrument you would use to confirm it before running the sketch again.
  1. The table below was recorded during a bench test of a chassis on blocks, using the duty sweep described in this chapter. The wheel speed was measured with a hand tachometer at each step.
Duty value Measured wheel speed (rpm)
30 0
60 0
75 22
120 61
180 98
255 142
  1. State the smallest duty value at which the wheel turns, and explain why the wheel does not turn below it.
  2. Using two figures from the table, state whether the wheel speed is proportional to the duty value, and support your answer with a calculation.
  3. A programmer wants the robot to creep towards a docking point as slowly as it reliably can. Recommend a duty value from this data and give one reason for your choice.

12.15 Case-Based Questions

  1. A school workshop is commissioning a sorting station. A belt is driven by a BO motor through an L298N module, supplied from a two-cell 7.4 V lithium polymer pack. The inputs available are an arm switch, a microswitch on the guard flap, an emergency-stop button with a normally-closed contact, an infrared sensor that reports when the collecting bin is full, and a divider across the pack presented at an analogue input.

The rule agreed with the workshop teacher is this: the belt runs only while the station is armed and the guard flap is closed; it must stop if the bin is full; it must stop if the emergency stop is opened; and it must stop, and stay stopped, if the pack falls below 6.6 V.

  1. Write the running rule as a single Boolean expression, naming each condition, and state which operator you have used at each point and why.
  2. Two of these four stopping conditions should latch until the station is reset, and two should be re-decided on every pass. State which are which, and justify each decision.
  3. Describe two tests you would run before the station is used by other students — one boundary case and one invalid case — stating for each what you would do, what you expect to happen, and how you would check it.
  1. A trolley carries supplies along a hospital corridor at a steady 0.4 m/s. It is required to stop when an obstacle is detected within 30 cm ahead. Its present program reads the ultrasonic sensor, prints a status line, and then calls delay(250), so one pass takes about 250 ms. A slotted encoder on the drive shaft produces about 400 pulses per second at this speed, and these are at present read with digitalRead() inside the same loop.
  1. Calculate how far the trolley travels between two consecutive sensor readings, and state what clearance actually remains when drive is removed. Comment on whether the 30 cm threshold is adequate.
  2. Explain why the encoder pulses cannot be counted reliably by this program, supporting your answer with the pulse period, and state what should be used instead.
  3. Describe how you would restructure the program so that the distance is measured every 100 ms, the encoder count is not lost, and a status line is still printed once every second. Name the mechanism used for each of the three, and state what you would measure to confirm that the restructuring has worked.