LABORATORY 07

Integrated Practical Applications

Duration: 4 hours (2 sessions) Material: Chapters 10-11 Integrates: Labs 1-6 PDF handout RO versiunea română

The last lab combines everything studied so far into complete applications: reading from sensors, driving power actuators, wireless communication, and distance measurement. Each application uses at least three of the previously studied modules.

1Lab objectives

  • Integrating the studied peripherals into a working application
  • Building a weather station with data display
  • Driving a DC motor with adjustable speed and direction
  • Sending data between two boards through a radio module
  • Measuring distance with an ultrasonic sensor and compensating for temperature
  • Structuring a complex program as a state machine

2Materials needed

  • 2 Arduino Uno boards
  • 1 DHT11 / DHT22 sensor
  • 1 DC motor + L298N H-bridge
  • 2 nRF24L01 modules
  • 1 HC-SR04 sensor
  • 1 10 kΩ potentiometer
  • 1 External 7-12 V supply
  • 2 10 µF capacitors

3Application 1 - Weather station

The DHT sensor measures temperature and humidity, sending the data through its own protocol, over a single wire. The timings are very strict (on the order of microseconds), which is why a dedicated library is used.

ParameterDHT11DHT22
Temperature range0...50 °C−40...80 °C
Temperature accuracy±2 °C±0.5 °C
Humidity range20...90%0...100%
Resolution1 °C / 1%0.1 °C / 0.1%
Minimum interval between readings1 s2 s
Supply3.3-5.5 V3.3-6 V
Connecting the DHT22 sensor to the Arduino Uno board
Fig. 2 - Connecting the DHT22 sensor to the Arduino Uno board. Fig. 7.2 in the handout
The minimum interval is not optionalReading more often than the specified interval returns NaN or the previous value, cached internally. Always check with isnan() before using the result.
weather_station.ino
#include <DHT.h>

#define DHT_PIN  7
#define DHT_TYPE DHT11

DHT sensor(DHT_PIN, DHT_TYPE);

unsigned long lastReading = 0;
const unsigned long INTERVAL = 2000;   // respects the minimum interval

float tMin = 999, tMax = -999;         // extreme values since power-on

void setup() {
  Serial.begin(9600);
  sensor.begin();
  Serial.println("Weather station started");
}

void loop() {
  if (millis() - lastReading < INTERVAL) return;   // non-blocking timing
  lastReading = millis();

  float humidity    = sensor.readHumidity();
  float temperature = sensor.readTemperature();

  if (isnan(humidity) || isnan(temperature)) {     // MANDATORY check
    Serial.println("Error reading the sensor!");
    return;
  }

  if (temperature < tMin) tMin = temperature;
  if (temperature > tMax) tMax = temperature;

  float heatIndex = sensor.computeHeatIndex(temperature, humidity, false);

  Serial.print("T = ");        Serial.print(temperature, 1);
  Serial.print(" C   RH = ");  Serial.print(humidity, 1);
  Serial.print(" %   feels like = "); Serial.print(heatIndex, 1);
  Serial.print(" C   [min ");  Serial.print(tMin, 1);
  Serial.print(" / max ");     Serial.print(tMax, 1);
  Serial.println("]");
}

4Application 2 - DC motor control

A motor cannot be powered directly from a microcontroller pin: it draws hundreds of milliamps, while a pin delivers at most 40 mA. An H-bridge is used, which allows both speed control through PWM and reversing the direction of rotation.

Arduino L298N H-bridge D9 (PWM) → ENA D4 → IN1 D5 → IN2 common GND M external 7-12 V supply the motor draws current from the external supply, NOT from the Arduino
Fig. 4 - Driving the motor through an H-bridge. The Arduino only provides the logic signals; the power current comes from the external supply.
IN1IN2ENA (PWM)Effect
00anystopped - free spinning (coast)
100...255forward rotation, speed set by PWM
010...255reverse rotation
11anyactive braking (shorting the terminals)
MandatoryPower the motor from an external supply, not from the USB port. The external supply's ground must be connected to the Arduino board's ground, otherwise the control signals have no common reference and the bridge does not switch correctly.
Why there is a minimum starting valueBelow a certain duty cycle, the torque produced does not overcome static friction: the motor just buzzes and heats up, without turning. This value is determined experimentally and is usually between 40 and 80 out of 255.

5Speed simulator

Adjust the duty cycle and watch the average voltage applied to the motor, as well as the resulting waveform. Compare the displayed value with the minimum starting threshold you will determine experimentally.

PWM - duty cycle, average voltage, and waveform
dc_motor_control.ino
const uint8_t IN1 = 4, IN2 = 5, ENA = 9;   // ENA must be a PWM pin
const uint8_t POT_PIN = A0;
const uint8_t MIN_DUTY = 60;               // below this the motor does not start

void setDirection(bool forward) {
  digitalWrite(IN1, forward ? HIGH : LOW);
  digitalWrite(IN2, forward ? LOW  : HIGH);
}

void stopMotor() {
  digitalWrite(IN1, LOW);
  digitalWrite(IN2, LOW);
  analogWrite(ENA, 0);
}

void setup() {
  pinMode(IN1, OUTPUT);
  pinMode(IN2, OUTPUT);
  pinMode(ENA, OUTPUT);
  Serial.begin(9600);
  stopMotor();
}

void loop() {
  uint16_t raw = analogRead(POT_PIN);

  // dead zone in the middle: below 490 backward, above 530 forward, in between stopped
  if (raw < 490) {
    setDirection(false);
    uint8_t duty = map(raw, 489, 0, MIN_DUTY, 255);
    analogWrite(ENA, duty);
    Serial.print("BACKWARD duty="); Serial.println(duty);
  }
  else if (raw > 530) {
    setDirection(true);
    uint8_t duty = map(raw, 531, 1023, MIN_DUTY, 255);
    analogWrite(ENA, duty);
    Serial.print("FORWARD  duty="); Serial.println(duty);
  }
  else {
    stopMotor();
    Serial.println("STOPPED");
  }

  delay(100);
}

6Application 3 - Radio communication

The nRF24L01 module operates in the 2.4 GHz band and communicates with the microcontroller over the SPI bus. It allows bidirectional links over tens of meters and automatically acknowledges received packets.

nRF24L01 pinArduino UnoRole
VCC3.3 Vpower - not 5 V!
GNDGNDground
CED9enable transmit/receive
CSND10SPI select (Slave Select)
SCKD13SPI clock
MOSID11data to the module
MISOD12data from the module
Power supply is the most common cause of malfunctionThe module has current spikes over 100 mA while transmitting, and the Arduino board's 3.3 V regulator only delivers 50 mA. Always add a 10 µF capacitor directly on the module's power pins. Without it, communication works intermittently or not at all.
Wireless communication diagram between two Arduino modules
Fig. 3 - The complete setup for radio communication between two boards: transmitter (left) and receiver (right). Fig. 7.10 in the handout
The data pins tolerate 5 VAlthough the supply must be 3.3 V, the module's SPI inputs accept 5 V levels, so no level converter is needed for SCK, MOSI, CE, and CSN.
radio_transmitter.ino
#include <SPI.h>
#include <RF24.h>

RF24 radio(9, 10);                        // CE, CSN
const byte address[6] = "NODE1";

struct Packet {                           // the structure MUST BE IDENTICAL on both boards
  uint16_t counter;
  int16_t  temperature;                   // in tenths of a degree, as an integer
  bool     button;
};

Packet data = {0, 0, false};

void setup() {
  Serial.begin(9600);
  pinMode(2, INPUT_PULLUP);

  radio.begin();
  radio.openWritingPipe(address);
  radio.setPALevel(RF24_PA_LOW);          // low power for bench testing
  radio.setDataRate(RF24_250KBPS);        // low speed -> longer range
  radio.stopListening();                  // transmit mode
}

void loop() {
  data.counter++;
  data.temperature = (int16_t)(analogRead(A0) * 0.489);   // example
  data.button = !digitalRead(2);

  bool sent = radio.write(&data, sizeof(data));

  Serial.print("Packet ");  Serial.print(data.counter);
  Serial.println(sent ? " - acknowledged" : " - FAILED");

  delay(500);
}
radio_receiver.ino
#include <SPI.h>
#include <RF24.h>

RF24 radio(9, 10);
const byte address[6] = "NODE1";

struct Packet {
  uint16_t counter;
  int16_t  temperature;
  bool     button;
};

Packet data;
uint16_t lastCounter = 0;

void setup() {
  Serial.begin(9600);
  pinMode(3, OUTPUT);                     // LED on another pin: D9-D13 are used by SPI

  radio.begin();
  radio.openReadingPipe(0, address);
  radio.setPALevel(RF24_PA_LOW);
  radio.setDataRate(RF24_250KBPS);
  radio.startListening();                 // receive mode
}

void loop() {
  if (radio.available()) {
    radio.read(&data, sizeof(data));

    digitalWrite(3, data.button);

    // detecting lost packets
    if (data.counter != lastCounter + 1 && lastCounter != 0) {
      Serial.print("!! lost ");
      Serial.print(data.counter - lastCounter - 1);
      Serial.println(" packets");
    }
    lastCounter = data.counter;

    Serial.print("Packet #"); Serial.print(data.counter);
    Serial.print("  T = ");   Serial.print(data.temperature / 10.0, 1);
    Serial.print(" C  button = "); Serial.println(data.button ? "pressed" : "released");
  }
}

7Application 4 - Distance measurement

The HC-SR04 sensor emits a 40 kHz ultrasonic pulse and measures the time until the echo returns. Knowing the speed of sound, the distance to the obstacle is calculated.

Arduino HC-SR04 5V → VCC D9 → TRIG D10 ← ECHO GND obstacle
Fig. 7 - A 10 µs pulse on TRIG triggers the measurement; the duration of the pulse on ECHO is proportional to the round-trip distance.
Timing diagram of the Trig and Echo signals on the ultrasonic sensor
Fig. 5 - How it works: a 10 µs Trig pulse, followed by a burst of 8 ultrasonic cycles, with the Echo signal's duration proportional to the distance. Fig. 7.13 in the handout
Practical setup for the distance measurement module
Fig. 6 - The practical setup for the distance measurement module. Fig. 7.14 in the handout
ParameterValueNotes
Measurement range2 cm ... 4 munder 2 cm the echo overlaps the emission
Accuracy±3 mmunder ideal conditions, flat perpendicular surface
Measurement angle~15°angled objects can reflect the echo elsewhere
TRIG pulse duration10 µsminimum needed to trigger
Maximum reading frequency~20 Hzwaits for residual echoes to die out

8Distance calculator

Change the echo duration and the air temperature. Notice the difference between the exact formula and the simplified one (t/58) - at large distances and extreme temperatures, the error becomes significant.

Echo duration → distance, with temperature compensation
distance_measurement.ino
const uint8_t TRIG_PIN = 9;
const uint8_t ECHO_PIN = 10;

float speedOfSound(float tempC) {
  return (331.3 + 0.606 * tempC) / 10000.0;   // cm/us
}

float measureDistance(float tempC) {
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);                       // the trigger pulse
  digitalWrite(TRIG_PIN, LOW);

  unsigned long duration = pulseIn(ECHO_PIN, HIGH, 30000UL);   // 30 ms limit

  if (duration == 0) return -1.0;              // no echo

  return (duration * speedOfSound(tempC)) / 2.0;  // round trip -> divide by 2
}

void setup() {
  Serial.begin(9600);
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(3, OUTPUT);
}

void loop() {
  float d = measureDistance(20.0);

  if (d < 0) {
    Serial.println("No echo - object too far");
    digitalWrite(3, LOW);
  } else {
    Serial.print("Distance = "); Serial.print(d, 1); Serial.println(" cm");
    digitalWrite(3, d < 20.0);                 // warning under 20 cm
  }

  delay(100);                                   // avoid interference between readings
}
Why temperature mattersThe speed of sound varies by roughly 0.6 m/s per degree Celsius. Between 0 °C and 30 °C the difference reaches almost 6%, so a 2 m measurement can be off by over 10 cm if a fixed value is used. Combining the HC-SR04 sensor with the DHT from the first application makes the compensation automatic.

9Integrating the applications

A program that does several things at once cannot use delay(). The recommended structure combines non-blocking timing with a state machine:

ModulePeriodMethod
Reading the DHT sensor2000 msnon-blocking millis()
Distance measurement100 msnon-blocking millis()
Motor controlevery iterationdirectly in loop()
Stop buttonimmediateinterrupt (Lab 3)
Radio transmission500 msnon-blocking millis()
multitasking_skeleton.ino
/* The four tasks are the functions written in the applications above.
   They are left empty here, so only the skeleton is clearly visible: */
void readDistance()    { /* application 4 */ }
void readTemperature() { /* application 1 */ }
void sendTelemetry()   { /* application 3 */ }
void updateMotor()     { /* application 2 */ }
void stopMotor()       { /* stops the motor's PWM */ }

unsigned long tDHT = 0, tDist = 0, tRadio = 0;
const unsigned long P_DHT = 2000, P_DIST = 100, P_RADIO = 500;

volatile bool emergencyStop = false;

void stopButton() { emergencyStop = true; }     // a very short ISR

void setup() {
  attachInterrupt(digitalPinToInterrupt(2), stopButton, FALLING);
}

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

  if (emergencyStop) {                            // has the highest priority
    stopMotor();
    return;
  }

  if (now - tDist >= P_DIST) {
    tDist = now;
    readDistance();
  }

  if (now - tDHT >= P_DHT) {
    tDHT = now;
    readTemperature();
  }

  if (now - tRadio >= P_RADIO) {
    tRadio = now;
    sendTelemetry();
  }

  updateMotor();                          // on every iteration
}

10Simulator: a state machine

An obstacle-avoiding robot cannot be written with delay(): it would go blind exactly when it needs the sensor. Watch how the state machine solves the problem, switching between states based on distance and time.

Obstacle-avoiding robot - a state machine
The advantage of the state machineOn every loop() iteration the sensor is read and the state is re-evaluated. There is never a moment when the robot "cannot see". With delay(600) instead of the BACKWARD state, the robot would be completely blind for 0.6 seconds.

11Interactive circuit

The last two circuits put together everything you have learned: reading a sensor, making a decision, driving an output, and running several tasks in parallel, without blocking the program.

Measuring distance with an ultrasonic sensor
const int TRIG_PIN = 11;
const int ECHO_PIN = 12;
const int LED_PIN  = 13;

void setup() {
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);
  pinMode(LED_PIN,  OUTPUT);
  Serial.begin(9600);
}

void loop() {
  /* the trigger pulse: 10 microseconds */
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);

  /* echo duration, in microseconds */
  unsigned long duration = pulseIn(ECHO_PIN, HIGH);

  /* sound travels at 343 m/s = 0.0343 cm/us, round trip */
  float distance = duration * 0.0343 / 2.0;

  Serial.print("echo = ");
  Serial.print(duration);
  Serial.print(" us   ->   ");
  Serial.print(distance);
  Serial.println(" cm");

  digitalWrite(LED_PIN, distance < 20 ? HIGH : LOW);
  delay(400);
}
Try thisMove the "obstacle" slider: the card shows you what echo duration the chosen distance produces, and the program converts it back into centimeters. Dividing by 2 is essential - the sound travels the path twice. Remove it and you will immediately see the distances double.
Exercise - three tasks in parallel, no delay
unsigned long tFast = 0, tSlow = 0, tSensor = 0;

const unsigned long P_FAST   = 200;
const unsigned long P_SLOW   = 1000;
const unsigned long P_SENSOR = 1500;

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

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

  if (now - tFast >= P_FAST) {
    tFast = now;
    digitalWrite(13, !digitalRead(13));
  }

  /* Complete this, following the same pattern:
       - every P_SLOW, toggle the LED on pin 12
       - every P_SENSOR, read A0 and print the value over serial
     Do not use delay() anywhere: the three tasks must
     run independently of one another. */
}
Try thisCompare the two traces on the oscilloscope: they must run completely independently. With delay() that would be impossible - a one-second pause would also stop the fast blink. The millis() pattern is the foundation of any serious microcontroller program.

12Work tasks

  • Build the weather station and print the temperature, humidity, and heat index over serial.
  • Add tracking of the minimum and maximum values since power-on.
  • Wire the H-bridge and drive the motor with variable speed and direction from the potentiometer.
  • Determine experimentally the minimum duty cycle at which the motor starts turning.
  • Measure the current drawn by the motor at different PWM values and correlate it with the speed.
  • Set up the radio link between two boards and transmit the weather station's data.
  • Detect and display lost packets, using the counter in the structure.
  • Implement distance measurement with temperature compensation from the DHT sensor.
  • Combine two applications: the distance sensor stops the motor when it detects an obstacle.
  • Rewrite a delay()-based program as a state machine and compare the responsiveness.

13Final project

An obstacle-avoiding mobile robotBuild a complete system that combines everything studied this semester:
  • two motors driven through an H-bridge, with speed set by PWM (Lab 4)
  • an HC-SR04 sensor for obstacle detection (Lab 5, Lab 7)
  • a start/emergency-stop button handled through an interrupt (Lab 3)
  • status LEDs driven through direct register access (Lab 2)
  • telemetry sent over serial or radio (Lab 6, Lab 7)
  • a state-machine structure, with no delay() anywhere in the main loop

Document the project with the circuit diagram, commented code, and a table of test results: stopping distance as a function of speed, for at least three different PWM values.

14Review questions

15Resources