Code Along - Chapter 16

Assessment Practice: Tasks A & B

Rehearse the two assessed practicals in the simulator: a two-button speed selector that reports every change, and a sensor alarm with hysteresis that fails safe when the sensor breaks.

Estimated time 90-120 minExercises passed 0 / 8

What this chapter is for

Two of the practical assessments are built and marked on the bench, with real buttons, a real sensor and a scope. This chapter rehearses the part that catches most people out: not the wiring, but the logic.

Work through the exercises here until they pass first time. Then you can spend the assessment sessions on the build, the measurements and the evidence, instead of debugging the logic.

Read this first. Passing the tests here is *not* the assessment and it is not evidence. The tasks are marked on your plan, your wiring, your measurements, your test table and your explanation. The simulator has no transistor, no oscilloscope and no thermistor - it only checks that your code decides the right thing.

Task A: one state variable runs everything

The fan has three speeds: OFF, LOW and HIGH. The temptation is to scatter analogWrite() calls all over the sketch. Don't. Keep one variable that says which state you are in, and one function that makes the outputs match it:

  • the buttons only ever change state
  • showState() sets the fan, sets the running LED and prints one line

That is why the brief asks for exactly one Serial line per change: if showState() is only called when the state actually changed, you get that for free. Printing in loop() instead floods the monitor and fails the criterion.

The duty values come straight from the brief: OFF is 0, LOW is 40% which is 102, and HIGH is 100% which is 255. Give them names - DUTY_OFF, DUTY_LOW and DUTY_HIGH - because the names make the sketch much easier to explain to the assessor.

StateFan D6 (PWM)Running LED D5Serial line
OFF0offFan: OFF (0%)
LOW102 (40%)onFan: LOW (40%)
HIGH255 (100%)onFan: HIGH (100%)
Worked example

The three states, stepped by a timer

No buttons yet - this just walks through the three states so you can watch the two LEDs and read the Serial Monitor. The named constants are worth copying: SPEED_LOW says far more than 1.

const int FAN = 6;
const int RUN_LED = 5;

const int SPEED_OFF = 0;
const int SPEED_LOW = 1;
const int SPEED_HIGH = 2;

const int DUTY_OFF = 0;      // 0%
const int DUTY_LOW = 102;    // 40% of 255
const int DUTY_HIGH = 255;   // 100%

int state = SPEED_OFF;

void setup() {
  Serial.begin(9600);
  pinMode(FAN, OUTPUT);
  pinMode(RUN_LED, OUTPUT);
  showState();
}

void loop() {
  delay(1000);
  state = state + 1;
  if (state > SPEED_HIGH) {
    state = SPEED_OFF;
  }
  showState();
}

void showState() {
  if (state == SPEED_OFF) {
    analogWrite(FAN, DUTY_OFF);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, DUTY_LOW);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, DUTY_HIGH);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: HIGH (100%)");
  }
}

Turn on JavaScript to edit, run and test this code in the browser.

Expected output
Fan: OFF (0%)
Fan: LOW (40%)
Fan: HIGH (100%)
Fan: OFF (0%)
  • The fan LED is on a ~ pin so it can dim. The running LED is only ever on or off.
  • showState() is called once in setup() so the outputs are right before anything is pressed.
  • delay(1000) is only here so the demo steps on its own. Your bench sketch steps on the buttons, using the debounce code given on the Task A brief.
Exercise 16.1

Make the outputs match the state

Not started

Before the buttons, get the outputs right. state is already declared; the tests set it to each of the three values in turn and check both LEDs.

In loop(), drive the fan on D6 and the running LED on D5 from state:

  • SPEED_OFF - fan DUTY_OFF, running LED off
  • SPEED_LOW - fan DUTY_LOW, running LED on
  • SPEED_HIGH - fan DUTY_HIGH, running LED on

To try it yourself, change int state = SPEED_OFF; to SPEED_LOW (or SPEED_HIGH) and press Run.

const int FAN = 6;
const int RUN_LED = 5;

const int SPEED_OFF = 0;
const int SPEED_LOW = 1;
const int SPEED_HIGH = 2;

const int DUTY_OFF = 0;      // 0%
const int DUTY_LOW = 102;    // 40% of 255
const int DUTY_HIGH = 255;   // 100%

int state = SPEED_OFF;

void setup() {
  pinMode(FAN, OUTPUT);
  pinMode(RUN_LED, OUTPUT);
}

void loop() {
  // Set the fan duty and the running LED to match state

}

Turn on JavaScript to edit, run and test this code in the browser.

Exercise 16.2

Button A steps through the speeds

Not started

showState() is written for you at the bottom, and setup() already calls it once so the outputs are right at start-up - that prints the first Fan: OFF (0%) line. All you have to write is the bit that changes state.

On each new press of Button A on D2, step OFF → LOW → HIGH → OFF and call showState() once. Nothing must be printed while the button is simply held down.

Sample output
Fan: OFF (0%)
Fan: LOW (40%)
Fan: HIGH (100%)
Fan: OFF (0%)
const int FAN = 6;
const int RUN_LED = 5;
const int BUTTON_A = 2;

const int SPEED_OFF = 0;
const int SPEED_LOW = 1;
const int SPEED_HIGH = 2;

const int DUTY_OFF = 0;      // 0%
const int DUTY_LOW = 102;    // 40% of 255
const int DUTY_HIGH = 255;   // 100%

int state = SPEED_OFF;
bool lastA = HIGH;

void setup() {
  Serial.begin(9600);
  pinMode(FAN, OUTPUT);
  pinMode(RUN_LED, OUTPUT);
  pinMode(BUTTON_A, INPUT_PULLUP);
  showState();
}

void loop() {
  bool nowA = digitalRead(BUTTON_A);
  // A new press: step to the next speed, wrap after HIGH, then showState();

  lastA = nowA;
}

void showState() {
  if (state == SPEED_OFF) {
    analogWrite(FAN, DUTY_OFF);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, DUTY_LOW);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, DUTY_HIGH);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: HIGH (100%)");
  }
}

Turn on JavaScript to edit, run and test this code in the browser.

Exercise 16.3

Button B stops the fan

Not started

Extra practice - Task A only needs Button A. Add Button B on D3. A new press returns the fan to SPEED_OFF from any speed.

One catch: the sketch prints one line per change. Pressing B when the fan is already off changes nothing, so it must print nothing.

Sample output
Fan: OFF (0%)
Fan: LOW (40%)
Fan: HIGH (100%)
Fan: OFF (0%)
Fan: LOW (40%)
const int FAN = 6;
const int RUN_LED = 5;
const int BUTTON_A = 2;
const int BUTTON_B = 3;

const int SPEED_OFF = 0;
const int SPEED_LOW = 1;
const int SPEED_HIGH = 2;

const int DUTY_OFF = 0;      // 0%
const int DUTY_LOW = 102;    // 40% of 255
const int DUTY_HIGH = 255;   // 100%

int state = SPEED_OFF;
bool lastA = HIGH;
bool lastB = HIGH;

void setup() {
  Serial.begin(9600);
  pinMode(FAN, OUTPUT);
  pinMode(RUN_LED, OUTPUT);
  pinMode(BUTTON_A, INPUT_PULLUP);
  pinMode(BUTTON_B, INPUT_PULLUP);
  showState();
}

void loop() {
  bool nowA = digitalRead(BUTTON_A);
  bool nowB = digitalRead(BUTTON_B);

  if (nowA == LOW && lastA == HIGH) {
    state = state + 1;
    if (state > SPEED_HIGH) {
      state = SPEED_OFF;
    }
    showState();
  }

  // A new press of B: go to SPEED_OFF - but only if the fan was running

  lastA = nowA;
  lastB = nowB;
}

void showState() {
  if (state == SPEED_OFF) {
    analogWrite(FAN, DUTY_OFF);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, DUTY_LOW);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, DUTY_HIGH);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: HIGH (100%)");
  }
}

Turn on JavaScript to edit, run and test this code in the browser.

Exercise 16.4

Twenty presses, twenty steps

DeeperNot started

Extra practice - not needed for Task A. This is the millis() way to debounce two buttons, for sketches that must keep doing other things while they wait. Real contacts chatter, and the test below now feeds your sketch that chatter: without debounce, two presses arrive as six steps.

The debounce timers are already written, with the same names as the Buttons & Debounce chapter - one per button. When a raw reading moves, that button's 20 ms clock starts again. Your job is the two blocks inside them: act on a button only when its trusted reading (stableA) is still HIGH and its settled reading (lastReadingA) is LOW.

Sample output
Fan: OFF (0%)
Fan: LOW (40%)
Fan: HIGH (100%)
const int FAN = 6;
const int RUN_LED = 5;
const int BUTTON_A = 2;
const int BUTTON_B = 3;

const int SPEED_OFF = 0;
const int SPEED_LOW = 1;
const int SPEED_HIGH = 2;

const int DUTY_OFF = 0;      // 0%
const int DUTY_LOW = 102;    // 40% of 255
const int DUTY_HIGH = 255;   // 100%

int state = SPEED_OFF;

bool stableA = HIGH;           // the reading we trust
bool lastReadingA = HIGH;      // the raw reading last time round
unsigned long changedAtA = 0;  // when the raw reading last changed
bool stableB = HIGH;
bool lastReadingB = HIGH;
unsigned long changedAtB = 0;

void setup() {
  Serial.begin(9600);
  pinMode(FAN, OUTPUT);
  pinMode(RUN_LED, OUTPUT);
  pinMode(BUTTON_A, INPUT_PULLUP);
  pinMode(BUTTON_B, INPUT_PULLUP);
  showState();
}

void loop() {
  bool readingA = digitalRead(BUTTON_A);
  bool readingB = digitalRead(BUTTON_B);

  if (readingA != lastReadingA) {
    changedAtA = millis();   // A moved - start its clock again
    lastReadingA = readingA;
  }
  if (readingB != lastReadingB) {
    changedAtB = millis();   // B moved - start its clock again
    lastReadingB = readingB;
  }

  if (millis() - changedAtA > 20) {
    // A has settled. A new press of A: step to the next speed and showState();

    stableA = lastReadingA;
  }

  if (millis() - changedAtB > 20) {
    // B has settled. A new press of B: SPEED_OFF, if the fan was running, and showState();

    stableB = lastReadingB;
  }
}

void showState() {
  if (state == SPEED_OFF) {
    analogWrite(FAN, DUTY_OFF);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, DUTY_LOW);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, DUTY_HIGH);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: HIGH (100%)");
  }
}

Turn on JavaScript to edit, run and test this code in the browser.

Task B: two thresholds, and never fail quiet

Task B watches a cabinet and sounds an alarm when it overheats. On the bench the sensor is a thermistor and you convert its reading to °C with the Beta equation. Here we work in raw ADC counts so the exercise is about the two decisions that are actually marked - hysteresis and the fail-safe. Drag the slider to change the reading; higher means hotter.

Hysteresis means using two thresholds instead of one. With a single threshold at 700, a reading that wobbles 699, 701, 699, 701 switches the alarm on and off several times a second. So: switch on at 700 or above, and only switch off again at 600 or below. Between 601 and 699 nothing changes - whatever the alarm is doing, it keeps doing.

Fail-safe means deciding what a broken sensor should do. An unplugged thermistor reads either 0 or 1023, and neither is a real temperature. An alarm that stays quiet because its sensor fell off is worse than useless, so a fault must switch the alarm on.

ReadingAlarm off nowAlarm on now
0 or 1023fault - switch onfault - stay on
601-699stay offstay on
700 and aboveswitch onstay on
600 and belowstay offswitch off
Worked example

What the sensor is doing

Run this, then drag the slider. It prints the reading twice a second so you can see the range the thresholds sit in. The alarm LED is not wired up yet - that is the next exercise.

const int SENSOR = A0;

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

void loop() {
  int reading = analogRead(SENSOR);
  Serial.print("Sensor: ");
  Serial.println(reading);
  delay(500);
}

Turn on JavaScript to edit, run and test this code in the browser.

Expected output
Sensor: 300
Sensor: 300
Sensor: 300
  • Dragging the slider while the sketch runs is the simulator's version of warming the thermistor with your hand.
  • 0 and 1023 are the ends of the slider - use them later to pretend the sensor has fallen off.
Exercise 16.5

One threshold

Not started

Start with the naive version, so you can see what is wrong with it.

Read A0 and switch the alarm LED on D8 on when the reading is 700 or more, and off when it is below 700.

const int SENSOR = A0;
const int ALARM_PIN = 8;
const int ON_LEVEL = 700;

void setup() {
  pinMode(ALARM_PIN, OUTPUT);
}

void loop() {
  int reading = analogRead(SENSOR);
  // Alarm on at ON_LEVEL or above, off below it

}

Turn on JavaScript to edit, run and test this code in the browser.

Exercise 16.6

Add the hysteresis

Not started

Now the real requirement. Keep a variable that remembers whether the alarm is on, and use two thresholds:

  • alarm off and the reading reaches 700 or more - switch on
  • alarm on and the reading falls to 600 or less - switch off
  • anywhere in between - leave it alone

The test walks the reading up to exactly 700, back down to 650, and only then down to exactly 600. The alarm must still be on at 650 on the way down, and off at 650 on the way up.

const int SENSOR = A0;
const int ALARM_PIN = 8;
const int ON_LEVEL = 700;
const int OFF_LEVEL = 600;

bool alarmOn = false;

void setup() {
  pinMode(ALARM_PIN, OUTPUT);
}

void loop() {
  int reading = analogRead(SENSOR);

  // Switch alarmOn on at ON_LEVEL, off at OFF_LEVEL, and leave it alone in between

  digitalWrite(ALARM_PIN, alarmOn);
}

Turn on JavaScript to edit, run and test this code in the browser.

Exercise 16.7

Fail safe, and report every change

Not started

The last two requirements together. There are now three states, and the constants and the reporting code are already written - you write the decision.

  • reading is 0 or 1023 - the sensor is broken: state FAULT
  • coming back from FAULT with a sensible reading - back to NORMAL
  • NORMAL and 700 or more - ALARM
  • ALARM and 600 or less - NORMAL

The alarm output is on in ALARM and in FAULT. That one line is the difference between a Competent and a Not Yet Competent on the fail-safe criterion.

Sample output
NORMAL
ALARM
NORMAL
SENSOR FAULT
NORMAL
const int SENSOR = A0;
const int ALARM_PIN = 8;
const int ON_LEVEL = 700;
const int OFF_LEVEL = 600;

const int NORMAL = 0;
const int ALARM = 1;
const int FAULT = 2;

int state = NORMAL;
int reported = -1;   // -1: nothing printed yet

void setup() {
  Serial.begin(9600);
  pinMode(ALARM_PIN, OUTPUT);
}

void loop() {
  int reading = analogRead(SENSOR);

  // Decide the new state: FAULT, ALARM or NORMAL

  digitalWrite(ALARM_PIN, state != NORMAL);

  if (state != reported) {
    reported = state;
    if (state == FAULT) {
      Serial.println("SENSOR FAULT");
    } else if (state == ALARM) {
      Serial.println("ALARM");
    } else {
      Serial.println("NORMAL");
    }
  }
}

Turn on JavaScript to edit, run and test this code in the browser.

Exercise 16.8

The state on the LCD

DeeperNot started

Task B is marked on the display too: *both LCD rows are clean in every state, with no leftover or overlapping characters*. SENSOR FAULT is 12 characters and ALARM is 5, so printing ALARM over SENSOR FAULT leaves ALARMR FAULT on the screen.

Finish showState() so row 2 (setCursor(0, 1)) always shows the state padded to 12 characters. Row 1 is already done for you.

#include <LiquidCrystal_I2C.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);

const int SENSOR = A0;
const int ALARM_PIN = 8;
const int ON_LEVEL = 700;
const int OFF_LEVEL = 600;

const int NORMAL = 0;
const int ALARM = 1;
const int FAULT = 2;

int state = NORMAL;
int reported = -1;   // -1: nothing printed yet

void setup() {
  lcd.init();
  lcd.backlight();
  pinMode(ALARM_PIN, OUTPUT);
}

void loop() {
  int reading = analogRead(SENSOR);

  if (reading == 0 || reading == 1023) {
    state = FAULT;
  } else if (state == FAULT) {
    state = NORMAL;
  } else if (state == NORMAL && reading >= ON_LEVEL) {
    state = ALARM;
  } else if (state == ALARM && reading <= OFF_LEVEL) {
    state = NORMAL;
  }

  digitalWrite(ALARM_PIN, state != NORMAL);

  if (state != reported) {
    reported = state;
    showState();
  }
}

void showState() {
  lcd.setCursor(0, 0);
  lcd.print("Cabinet monitor");

  lcd.setCursor(0, 1);
  // Print the state, padded to 12 characters, so nothing is left behind

}

Turn on JavaScript to edit, run and test this code in the browser.

Taking it to the bench

What you have written here is the middle of each task. The marks are in what surrounds it, so plan for those too:

  • Task A - exercises 16.1 to 16.3 are the whole of Task A's logic. On the bench add the plan sheet (pins and speeds) before you wire anything, the scope reading of D6 at LOW (about 980 Hz, 40% duty) and the test table on the brief. Real buttons bounce, so add the 50 ms button code from the brief - the assessor will press ten times quickly and count.
  • Task B - the plan sheet with the transistor stage drawn, the calibration table, and the transistor stage itself. The buzzer never goes straight onto a pin. The assessor will unplug your thermistor mid-demonstration. Do the 0 / 1023 fault check on the raw ADC reading, before you call readCelsius() from the brief - a broken sensor gives a nonsense temperature, not a clear fault.

Two differences to watch for when you move this code to real hardware. First, the readings here are raw ADC counts; on the bench you convert to °C first and compare against 30.0 and 28.0, using float for the temperature. Second, an NTC thermistor reading gets smaller as it gets hotter, so once you have wired yours, check which way your divider goes before you decide which comparison is >=.

The full briefs, the evidence lists and the marking grids are on the Practical Assessments page.