Facilitator guide

Assessment Model Answers

A working reference sketch for each assessment, checked to compile for the Uno. Use them to check your own bench set-up and to mark. A learner's sketch does not need to match: mark the behaviour, the evidence and the explanation against the criteria grid.

Contents

  1. Code-Correction Exercise
  2. Practical Task A: Extractor Fan Speed Selector
  3. Practical Task B: Cabinet Over-Temperature Alarm
  4. Practical Task C: Greenhouse Vent Controller
  5. Final Project: Individual Microcontroller System

Code-Correction Exercise

The given sketch with only the faults fixed. Six faults cover the five classes. Open the brief.

Fault inventory answer

LineFaultClass
2 and 12ButtonPin declared, buttonPin usedCapitalisation
6PinMode instead of pinModeCapitalisation
6INPUT, but the button is wired to GND (floating input)Pin mode vs wiring
7Missing ; after pinMode(indicatorPin, OUTPUT)Semicolon
13if (pressed = true) assigns instead of comparing= versus ==
15Missing } before elseBraces

What to check

  • Line numbers refer to the given sketch on the brief (line 1 is the comment).
  • With if (pressed = true) left in, the LED stays on whether or not the button is pressed.
  • Tidy-ups such as adding const are fine but are not faults.

Reference sketch

// Workshop door-bell indicator - MODEL ANSWER
// Each fixed line says which fault it was.
byte buttonPin = 2;       // capitalisation: was ButtonPin, but loop() uses buttonPin
byte indicatorPin = 13;

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);  // capitalisation: was PinMode
                                     // pin mode: was INPUT, but the button goes to GND
  pinMode(indicatorPin, OUTPUT);     // semicolon: the ; was missing
  Serial.begin(9600);
}

void loop() {
  bool pressed = digitalRead(buttonPin) == LOW;  // pressed reads LOW
  if (pressed == true) {             // = versus ==: = stores, == compares
    digitalWrite(indicatorPin, HIGH);
  } else {                           // braces: the } before else was missing
    digitalWrite(indicatorPin, LOW);
  }
}

Practical Task A: Extractor Fan Speed Selector

One button, the 50 ms button code from the brief, and a speed variable that drives both LEDs. The same logic as Code Along 16.1-16.3. Open the brief.

What to check

  • Serial shows Fan: OFF (0%) once at reset, then exactly one line per press.
  • Ten quick presses from OFF end at LOW (10 steps: three full rounds plus one).
  • D6 is a Timer0 pin, so the scope shows about 980 Hz (not 490 Hz). LOW reads 40 % duty.
  • Named constants for the speeds are nice but not required - plain 0, 102 and 255 with comments are fine for a first task.
  • A learner sketch may differ in names and layout. Mark the behaviour and the explanation, not a match with this code.

Reference sketch

// Task A: Extractor fan speed selector - MODEL ANSWER
// Each press of the button steps OFF -> LOW -> HIGH -> OFF.

const byte buttonPin = 2;     // push button, D2 to GND
const byte runLed = 5;        // running LED (330 ohm resistor)
const byte fanPin = 6;        // fan LED on a PWM pin (330 ohm resistor)

const int SPEED_OFF = 0;      // the three speeds
const int SPEED_LOW = 1;
const int SPEED_HIGH = 2;

int speed = SPEED_OFF;        // the current speed
bool lastReading = HIGH;      // HIGH = not pressed (from the brief)

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(runLed, OUTPUT);
  pinMode(fanPin, OUTPUT);
  Serial.begin(9600);
  showSpeed();                // start at OFF and print it once
}

void loop() {
  // Button code from the brief
  bool reading = digitalRead(buttonPin);
  if (reading != lastReading) {       // the button has just changed
    if (reading == LOW) {             // just pressed: next speed
      speed = speed + 1;
      if (speed > SPEED_HIGH) {
        speed = SPEED_OFF;            // after HIGH comes OFF
      }
      showSpeed();
    }
    delay(50);                        // let the bounce die away
    lastReading = reading;
  }
}

// Make the LEDs match the speed and print ONE line.
// Called only when the speed changes, so Serial never floods.
void showSpeed() {
  if (speed == SPEED_OFF) {
    analogWrite(fanPin, 0);           // 0 %
    digitalWrite(runLed, LOW);
    Serial.println("Fan: OFF (0%)");
  } else if (speed == SPEED_LOW) {
    analogWrite(fanPin, 102);         // 40 % of 255
    digitalWrite(runLed, HIGH);
    Serial.println("Fan: LOW (40%)");
  } else {
    analogWrite(fanPin, 255);         // 100 %
    digitalWrite(runLed, HIGH);
    Serial.println("Fan: HIGH (100%)");
  }
}

Practical Task B: Cabinet Over-Temperature Alarm

Uses the readCelsius() function given on the brief, two thresholds and a broken-sensor check. A delay(500) is allowed in this task. Open the brief.

What to check

  • Unplugged thermistor: A0 is pulled to 5 V and reads 1023, so SENSOR FAULT shows and the alarm turns on.
  • The 0 / 1023 check must come before readCelsius() - with 1023 the formula divides by zero.
  • Between 28 °C and 30 °C the alarm keeps whatever it was doing - watch it stay on while the sensor cools.
  • offsetC is where a learner puts a calibration correction from their table.
  • Check the driver: D8 to a 1 kΩ base resistor, emitter to GND, buzzer and LED (with its resistor) between +5 V and the collector.

Reference sketch

// Task B: Cabinet over-temperature alarm - MODEL ANSWER
// Thermistor divider on A0: 10k from 5 V to A0, thermistor from A0 to GND.
// D8 -> 1k -> NPN base. The transistor switches the buzzer and red LED.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

const byte sensorPin = A0;
const byte alarmPin = 8;          // to the transistor's 1k base resistor

const float alarmOnC = 30.0;      // switch ON at or above this
const float alarmOffC = 28.0;     // switch OFF at or below this
const float offsetC = 0.0;        // calibration correction from your table

LiquidCrystal_I2C lcd(0x27, 16, 2);   // try 0x3F if the screen stays blank

bool alarmOn = false;             // remembers the alarm between passes

void setup() {
  pinMode(alarmPin, OUTPUT);
  lcd.init();
  lcd.backlight();
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(sensorPin);

  // Broken or unplugged sensor: alarm ON.
  if (raw <= 0 || raw >= 1023) {
    alarmOn = true;
    digitalWrite(alarmPin, HIGH);
    lcd.setCursor(0, 0);
    lcd.print("Temp: ---       ");
    lcd.setCursor(0, 1);
    lcd.print("SENSOR FAULT    ");
    Serial.println("SENSOR FAULT");
    delay(500);
    return;                       // skip the rest of loop() this time
  }

  float celsius = readCelsius(raw) + offsetC;

  // Two thresholds. Between 28 and 30 nothing changes.
  if (celsius >= alarmOnC) {
    alarmOn = true;
  }
  if (celsius <= alarmOffC) {
    alarmOn = false;
  }

  if (alarmOn) {
    digitalWrite(alarmPin, HIGH);
  } else {
    digitalWrite(alarmPin, LOW);
  }

  // Row 1: temperature. Spaces at the end wipe old characters.
  lcd.setCursor(0, 0);
  lcd.print("Temp: ");
  lcd.print(celsius, 1);
  lcd.print(" C    ");

  // Row 2: the state.
  lcd.setCursor(0, 1);
  if (alarmOn) {
    lcd.print("ALARM           ");
  } else {
    lcd.print("NORMAL          ");
  }

  Serial.println(celsius, 1);
  delay(500);                     // two updates a second is plenty
}

// Given on the brief: turns the A0 reading into degrees C.
float readCelsius(int raw) {
  float resistance = 10000.0 / (1023.0 / raw - 1.0);   // thermistor in ohms
  float invT = 1.0 / 298.15 + log(resistance / 10000.0) / 3950.0;
  return 1.0 / invT - 273.15;                           // kelvin to C
}

Practical Task C: Greenhouse Vent Controller

Built on the Capstone 1 sketch with the loop() layout from the brief: one millis() timer for the DHT11, the button checked on every pass. Open the brief.

What to check

  • Unplugging the DHT11 shows ERR and keeps the last good values, so the vent does not move in AUTO.
  • checkButton() is the Task A button code. Its 50 ms wait and the DHT11 read (about 25 ms every 2 s) are both short, so the button still answers straight away.
  • LCD row 2 here reads the mode then the vent (AUTO vent OPEN). Any short wording is fine if both rows stay clean.
  • Servo power: the servo's red wire goes to the separate 5 V supply, not the Uno, with the grounds joined.

Reference sketch

// Task C: Greenhouse vent controller - MODEL ANSWER
// Built on the Capstone 1 sketch: DHT11 on D2, I2C LCD on A4/A5.
// Added: servo vent on D9 (own 5 V supply, common GND), mode button D3 to GND.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <DHT.h>
#include <Servo.h>

const byte dhtPin = 2;
const byte buttonPin = 3;
const byte servoPin = 9;

const int VENT_CLOSED = 0;        // servo angles
const int VENT_OPEN = 90;
const float openC = 28.0;         // AUTO: open at or above this
const float closeC = 26.0;        // AUTO: close at or below this

const int MODE_AUTO = 0;          // the three modes
const int MODE_OPEN = 1;
const int MODE_CLOSED = 2;

LiquidCrystal_I2C lcd(0x27, 16, 2);
DHT dht(dhtPin, DHT11);
Servo vent;

int mode = MODE_AUTO;
bool ventOpen = false;
bool dhtOk = false;               // was the last DHT reading good?
float tempC = 0;                  // last GOOD readings
float humidity = 0;
unsigned long lastRead = 0;

bool lastReading = HIGH;          // button code from Task A

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  vent.attach(servoPin);
  vent.write(VENT_CLOSED);
  lcd.init();
  lcd.backlight();
  dht.begin();
  Serial.begin(9600);
  showLcd();
}

// The loop() layout given on the brief.
void loop() {
  checkButton();                         // every pass: the button always answers

  if (millis() - lastRead >= 2000) {     // every 2 s, no delay()
    lastRead = millis();
    readDHT();
    updateVent();
    showLcd();
  }
}

void checkButton() {                     // the button code from Task A
  bool reading = digitalRead(buttonPin);
  if (reading != lastReading) {           // the button has just changed
    if (reading == LOW) {                 // just pressed: next mode
      mode = mode + 1;
      if (mode > MODE_CLOSED) {
        mode = MODE_AUTO;
      }
      updateVent();
      showLcd();
    }
    delay(50);                            // let the bounce die away
    lastReading = reading;
  }
}

void readDHT() {
  float t = dht.readTemperature();
  float h = dht.readHumidity();
  if (isnan(t) || isnan(h)) {
    dhtOk = false;                        // keep the last good values
    Serial.println("DHT read failed");
  } else {
    dhtOk = true;
    tempC = t;
    humidity = h;
  }
}

void updateVent() {
  bool wantOpen = ventOpen;               // default: leave it where it is
  if (mode == MODE_OPEN) {
    wantOpen = true;
  } else if (mode == MODE_CLOSED) {
    wantOpen = false;
  } else if (dhtOk && tempC >= openC) {   // AUTO and warm: open
    wantOpen = true;
  } else if (dhtOk && tempC <= closeC) {  // AUTO and cool: closed
    wantOpen = false;
  }                                       // failed read: no change

  if (wantOpen != ventOpen) {             // move only on a change
    ventOpen = wantOpen;
    if (ventOpen) {
      vent.write(VENT_OPEN);
    } else {
      vent.write(VENT_CLOSED);
    }
  }
}

void showLcd() {
  // Row 1: 27.0C 61%   (ERR when the last read failed)
  lcd.setCursor(0, 0);
  lcd.print(tempC, 1);
  lcd.print("C ");
  lcd.print(humidity, 0);
  lcd.print("% ");
  if (dhtOk) {
    lcd.print("      ");
  } else {
    lcd.print("ERR   ");
  }

  // Row 2: AUTO OPEN
  lcd.setCursor(0, 1);
  if (mode == MODE_AUTO) {
    lcd.print("AUTO   ");
  } else if (mode == MODE_OPEN) {
    lcd.print("OPEN   ");
  } else {
    lcd.print("CLOSED ");
  }
  if (ventOpen) {
    lcd.print("vent OPEN");
  } else {
    lcd.print("vent SHUT");
  }
}

Final Project: Individual Microcontroller System

Not a model answer - every project is different. This is the desk-fan example from the brief, built to the minimum scope, to show the expected size and level. Open the brief.

What to check

  • Minimum scope check: thermistor (analogue) and button (2 inputs), motor through a transistor (actuator), LCD (interface), AUTO/ON/OFF (states), millis() (timing).
  • About 120 lines is a realistic size for a beginner final project. Bigger is not better.
  • A broken sensor is treated as hot so the fan runs - a sensible fail-safe a learner could explain.

Reference sketch

// Final project EXAMPLE - one possible project, not the only answer.
// Desk fan: a thermistor sets the speed in AUTO; a button picks AUTO, ON or OFF.
// Thermistor divider on A0, button D2 to GND, I2C LCD on A4/A5.
// Motor on D5 through an NPN transistor (1k base resistor, flyback diode
// across the motor) and its own supply with a common GND.
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <math.h>

const byte sensorPin = A0;        // input 1: analogue
const byte buttonPin = 2;         // input 2: mode button
const byte motorPin = 5;          // actuator: PWM pin to the transistor

const int MODE_AUTO = 0;
const int MODE_ON = 1;
const int MODE_OFF = 2;

const float startC = 25.0;        // AUTO: fan starts here...
const float fullC = 32.0;         // ...and is at full speed here
const unsigned long updateMs = 500;
const unsigned long debounceMs = 30;

LiquidCrystal_I2C lcd(0x27, 16, 2);

int mode = MODE_AUTO;
float celsius = 0;
int duty = 0;
unsigned long lastUpdate = 0;
bool stableBtn = HIGH;
bool lastBtn = HIGH;
unsigned long changedBtn = 0;

void setup() {
  pinMode(buttonPin, INPUT_PULLUP);
  pinMode(motorPin, OUTPUT);
  lcd.init();
  lcd.backlight();
}

void loop() {
  checkButton();
  if (millis() - lastUpdate >= updateMs) {   // twice a second, no delay()
    lastUpdate = millis();
    celsius = readCelsius();
    setFan();
    showLcd();
  }
}

void checkButton() {
  bool reading = digitalRead(buttonPin);
  if (reading != lastBtn) {
    changedBtn = millis();
    lastBtn = reading;
  }
  if (millis() - changedBtn >= debounceMs && reading != stableBtn) {
    stableBtn = reading;
    if (stableBtn == LOW) {               // a new press: next mode
      mode = mode + 1;
      if (mode > MODE_OFF) {
        mode = MODE_AUTO;
      }
      setFan();
      showLcd();
    }
  }
}

float readCelsius() {             // Beta equation from the thermistor lesson
  int raw = analogRead(sensorPin);
  if (raw <= 0 || raw >= 1023) {
    return 99.0;                  // broken sensor: treat as hot, fan runs
  }
  float resistance = 10000.0 / (1023.0 / raw - 1.0);
  float invT = 1.0 / 298.15 + log(resistance / 10000.0) / 3950.0;
  return 1.0 / invT - 273.15;
}

void setFan() {
  if (mode == MODE_ON) {
    duty = 255;
  } else if (mode == MODE_OFF) {
    duty = 0;
  } else {                        // AUTO: warmer = faster
    duty = map(celsius * 10, startC * 10, fullC * 10, 0, 255);
    duty = constrain(duty, 0, 255);
  }
  analogWrite(motorPin, duty);
}

void showLcd() {
  lcd.setCursor(0, 0);
  lcd.print("Temp ");
  lcd.print(celsius, 1);
  lcd.print(" C    ");
  lcd.setCursor(0, 1);
  if (mode == MODE_AUTO) {
    lcd.print("AUTO ");
  } else if (mode == MODE_ON) {
    lcd.print("ON   ");
  } else {
    lcd.print("OFF  ");
  }
  lcd.print("fan ");
  lcd.print(duty * 100L / 255);
  lcd.print("%    ");
}