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.

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;

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, 0);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, 102);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, 255);
    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.
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 0, running LED off
  • SPEED_LOW - fan 102, running LED on
  • SPEED_HIGH - fan 255, running LED on
const int FAN = 6;
const int RUN_LED = 5;

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

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. 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: 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;

int state = SPEED_OFF;
bool lastA = HIGH;

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

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, 0);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, 102);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, 255);
    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

Add Button B on D3. A new press returns the fan to SPEED_OFF from any speed.

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

Sample output
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;

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);
}

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, 0);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, 102);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, 255);
    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

The assessor will press Button A twenty times quickly and count the steps. Real contacts chatter, and the test below now feeds your sketch that chatter: two presses arrive as five steps.

The debounce timer is already written - it notices when either raw reading moves and restarts a 20 ms clock. Your job is the two blocks inside it: act on a button only when its trusted reading is HIGH and the settled raw reading is LOW.

Sample output
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;

int state = SPEED_OFF;

bool rawA = HIGH;          // what the pin read last time round
bool rawB = HIGH;
bool steadyA = HIGH;       // the reading we trust
bool steadyB = HIGH;
unsigned long lastChange = 0;

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

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

  if (nowA != rawA || nowB != rawB) {
    rawA = nowA;
    rawB = nowB;
    lastChange = millis();   // something moved - start the clock again
  }

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

    // A new press of B: SPEED_OFF, if the fan was running, and showState();

    steadyA = rawA;
    steadyB = rawB;
  }
}

void showState() {
  if (state == SPEED_OFF) {
    analogWrite(FAN, 0);
    digitalWrite(RUN_LED, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (state == SPEED_LOW) {
    analogWrite(FAN, 102);
    digitalWrite(RUN_LED, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(FAN, 255);
    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 past 700, back down to 650, and only then down to 550. 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;

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 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;

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 - the state table on paper before you wire anything, the LED current calculation, the scope measurement of D6 at LOW (about 490 Hz, 40% duty) and a test table with real observed results. The assessor will press Button A twenty times and count.
  • Task B - the Beta equation with your own R0, T0 and B, a calibration table against a reference thermometer, and the transistor or MOSFET stage. The buzzer never goes straight onto a pin. The assessor will unplug your thermistor mid-demonstration.

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.